@shopbite-de/storefront 1.18.5 → 1.20.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/.github/workflows/ci.yaml +48 -48
- package/app/components/Category/Listing.vue +41 -95
- package/app/components/Product/Configurator.vue +4 -9
- package/app/components/Product/CrossSelling.vue +7 -50
- package/app/components/Product/Detail.vue +7 -119
- package/app/composables/useCategory.ts +6 -34
- package/app/composables/useCategoryListing.ts +131 -0
- package/app/composables/useProductConfigurator.ts +12 -52
- package/app/composables/useProductCrossSelling.ts +45 -0
- package/app/composables/useProductDetail.ts +62 -0
- package/app/pages/bestellung/bestaetigen.vue +9 -0
- package/app/pages/bestellung/warenkorb.vue +9 -0
- package/app/pages/bestellung/zahlung-versand.vue +9 -0
- package/nuxt.config.ts +9 -4
- package/package.json +1 -1
- package/server/api/category/[categoryId].get.ts +26 -0
- package/server/api/listing/[categoryId].get.ts +107 -0
- package/server/api/product/[productId]/cross-selling.get.ts +31 -0
- package/server/api/product/[productId].get.ts +68 -0
- package/server/api/product/variant.get.ts +99 -0
- package/test/nuxt/useProductConfigurator.test.ts +14 -17
|
@@ -121,51 +121,51 @@ jobs:
|
|
|
121
121
|
- name: Unit tests
|
|
122
122
|
run: pnpm test:unit
|
|
123
123
|
|
|
124
|
-
e2e-test:
|
|
125
|
-
name: E2E tests
|
|
126
|
-
environment: test
|
|
127
|
-
needs: [setup]
|
|
128
|
-
timeout-minutes: 15
|
|
129
|
-
runs-on: ubuntu-latest
|
|
130
|
-
steps:
|
|
131
|
-
- uses: actions/checkout@v6
|
|
132
|
-
|
|
133
|
-
- uses: pnpm/action-setup@v5
|
|
134
|
-
with:
|
|
135
|
-
version: 10.33.0
|
|
136
|
-
|
|
137
|
-
- uses: actions/setup-node@v6
|
|
138
|
-
with:
|
|
139
|
-
node-version: '24'
|
|
140
|
-
|
|
141
|
-
- name: Restore workspace from cache
|
|
142
|
-
uses: actions/cache/restore@v5
|
|
143
|
-
with:
|
|
144
|
-
path: |
|
|
145
|
-
~/.local/share/pnpm/store
|
|
146
|
-
node_modules
|
|
147
|
-
.nuxt
|
|
148
|
-
.output
|
|
149
|
-
key: workspace-${{ runner.os }}-${{ github.sha }}
|
|
150
|
-
|
|
151
|
-
- name: Cache Playwright browsers
|
|
152
|
-
uses: actions/cache@v5
|
|
153
|
-
with:
|
|
154
|
-
path: ~/.cache/ms-playwright
|
|
155
|
-
key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
156
|
-
|
|
157
|
-
- name: Install Playwright browsers
|
|
158
|
-
run: pnpm exec playwright install --with-deps chromium
|
|
159
|
-
|
|
160
|
-
- name: E2E tests
|
|
161
|
-
env:
|
|
162
|
-
TEST_USER: ${{ secrets.TEST_USER }}
|
|
163
|
-
TEST_USER_PASS: ${{ secrets.TEST_USER_PASS }}
|
|
164
|
-
run: pnpm test:e2e
|
|
165
|
-
|
|
166
|
-
- uses: actions/upload-artifact@v7
|
|
167
|
-
if: ${{ !cancelled() }}
|
|
168
|
-
with:
|
|
169
|
-
name: playwright-report
|
|
170
|
-
path: playwright-report/
|
|
171
|
-
retention-days: 30
|
|
124
|
+
# e2e-test:
|
|
125
|
+
# name: E2E tests
|
|
126
|
+
# environment: test
|
|
127
|
+
# needs: [setup]
|
|
128
|
+
# timeout-minutes: 15
|
|
129
|
+
# runs-on: ubuntu-latest
|
|
130
|
+
# steps:
|
|
131
|
+
# - uses: actions/checkout@v6
|
|
132
|
+
#
|
|
133
|
+
# - uses: pnpm/action-setup@v5
|
|
134
|
+
# with:
|
|
135
|
+
# version: 10.33.0
|
|
136
|
+
#
|
|
137
|
+
# - uses: actions/setup-node@v6
|
|
138
|
+
# with:
|
|
139
|
+
# node-version: '24'
|
|
140
|
+
#
|
|
141
|
+
# - name: Restore workspace from cache
|
|
142
|
+
# uses: actions/cache/restore@v5
|
|
143
|
+
# with:
|
|
144
|
+
# path: |
|
|
145
|
+
# ~/.local/share/pnpm/store
|
|
146
|
+
# node_modules
|
|
147
|
+
# .nuxt
|
|
148
|
+
# .output
|
|
149
|
+
# key: workspace-${{ runner.os }}-${{ github.sha }}
|
|
150
|
+
#
|
|
151
|
+
# - name: Cache Playwright browsers
|
|
152
|
+
# uses: actions/cache@v5
|
|
153
|
+
# with:
|
|
154
|
+
# path: ~/.cache/ms-playwright
|
|
155
|
+
# key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
156
|
+
#
|
|
157
|
+
# - name: Install Playwright browsers
|
|
158
|
+
# run: pnpm exec playwright install --with-deps chromium
|
|
159
|
+
#
|
|
160
|
+
# - name: E2E tests
|
|
161
|
+
# env:
|
|
162
|
+
# TEST_USER: ${{ secrets.TEST_USER }}
|
|
163
|
+
# TEST_USER_PASS: ${{ secrets.TEST_USER_PASS }}
|
|
164
|
+
# run: pnpm test:e2e
|
|
165
|
+
#
|
|
166
|
+
# - uses: actions/upload-artifact@v7
|
|
167
|
+
# if: ${{ !cancelled() }}
|
|
168
|
+
# with:
|
|
169
|
+
# name: playwright-report
|
|
170
|
+
# path: playwright-report/
|
|
171
|
+
# retention-days: 30
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
3
|
import Breadcrumb from "~/components/Category/Breadcrumb.vue";
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{
|
|
@@ -8,72 +8,43 @@ const props = defineProps<{
|
|
|
8
8
|
|
|
9
9
|
const { id: categoryId } = toRefs(props);
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
includes: {
|
|
13
|
-
product: [
|
|
14
|
-
"id",
|
|
15
|
-
"productNumber",
|
|
16
|
-
"name",
|
|
17
|
-
"description",
|
|
18
|
-
"calculatedPrice",
|
|
19
|
-
"translated",
|
|
20
|
-
"properties",
|
|
21
|
-
"propertyIds",
|
|
22
|
-
"sortedProperties",
|
|
23
|
-
"cover",
|
|
24
|
-
],
|
|
25
|
-
property: ["id", "name", "translated", "options"],
|
|
26
|
-
property_group_option: ["id", "name", "translated", "group"],
|
|
27
|
-
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
28
|
-
},
|
|
29
|
-
associations: {
|
|
30
|
-
cover: {
|
|
31
|
-
associations: {
|
|
32
|
-
media: {},
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
properties: {
|
|
36
|
-
associations: {
|
|
37
|
-
group: {},
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
limit: 100,
|
|
42
|
-
} as operations["searchPage post /search"]["body"];
|
|
11
|
+
const { category } = await useCategory(categoryId);
|
|
43
12
|
|
|
44
13
|
const {
|
|
45
|
-
|
|
14
|
+
showSkeleton,
|
|
46
15
|
loading,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
setInitialListing,
|
|
57
|
-
} = useListing({
|
|
58
|
-
listingType: "categoryListing",
|
|
59
|
-
categoryId: props.id,
|
|
60
|
-
defaultSearchCriteria: searchCriteria,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const { category } = await useCategory(categoryId);
|
|
16
|
+
elements,
|
|
17
|
+
sortingOrders,
|
|
18
|
+
currentSortingOrder,
|
|
19
|
+
availableFilters,
|
|
20
|
+
currentFilters,
|
|
21
|
+
changeSorting,
|
|
22
|
+
setFilters,
|
|
23
|
+
resetFilters,
|
|
24
|
+
} = useCategoryListing(props.id);
|
|
64
25
|
|
|
65
26
|
useCategorySeo(category);
|
|
66
27
|
|
|
67
|
-
const currentSorting = ref(
|
|
28
|
+
const currentSorting = ref(currentSortingOrder.value ?? "Sortieren");
|
|
29
|
+
|
|
30
|
+
// Sync currentSorting when listing data arrives during client-side navigation
|
|
31
|
+
// (currentSortingOrder.value is null at setup time in that case).
|
|
32
|
+
watch(
|
|
33
|
+
currentSortingOrder,
|
|
34
|
+
(val) => {
|
|
35
|
+
if (val) currentSorting.value = val;
|
|
36
|
+
},
|
|
37
|
+
{ once: true },
|
|
38
|
+
);
|
|
68
39
|
|
|
69
40
|
const propertyFilters = computed<Schemas["PropertyGroup"][]>(
|
|
70
41
|
() =>
|
|
71
|
-
(
|
|
42
|
+
(availableFilters.value?.filter(
|
|
72
43
|
(availableFilter) => availableFilter.code === "properties",
|
|
73
44
|
) ?? []) as unknown as Schemas["PropertyGroup"][],
|
|
74
45
|
);
|
|
75
46
|
|
|
76
|
-
const selectedPropertyFilters = ref(
|
|
47
|
+
const selectedPropertyFilters = ref(currentFilters.value?.properties ?? []);
|
|
77
48
|
const selectedPropertyFiltersString = computed(() =>
|
|
78
49
|
selectedPropertyFilters.value?.join("|"),
|
|
79
50
|
);
|
|
@@ -87,48 +58,26 @@ const selectedListingFilters = computed<ShortcutFilterParam[]>(() => {
|
|
|
87
58
|
];
|
|
88
59
|
});
|
|
89
60
|
|
|
90
|
-
|
|
91
|
-
const { data: listingPayload, pending } = await useAsyncData(
|
|
92
|
-
`listing${categoryId.value}`,
|
|
93
|
-
async () => {
|
|
94
|
-
await search(searchCriteria);
|
|
95
|
-
// Return the result so it gets serialized into the SSR payload.
|
|
96
|
-
// On the client, useAsyncData will restore this without re-running search().
|
|
97
|
-
return getCurrentListing.value;
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Populate useListing state from the SSR payload on the client.
|
|
102
|
-
// useListing uses plain refs (not useState), so its state is not automatically
|
|
103
|
-
// hydrated — we restore it via setInitialListing.
|
|
104
|
-
if (listingPayload.value) {
|
|
105
|
-
await setInitialListing(listingPayload.value);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// During SSR hydration, pending may briefly be true before the payload cache is applied.
|
|
109
|
-
// Suppress the skeleton in that window to prevent a hydration mismatch.
|
|
110
|
-
const showSkeleton = computed(() => pending.value && !nuxtApp.isHydrating);
|
|
61
|
+
let filterChain = Promise.resolve();
|
|
111
62
|
|
|
112
63
|
watch(selectedListingFilters, (newFilters, oldFilters) => {
|
|
113
|
-
if (newFilters[0]?.value === oldFilters?.[0]?.value)
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
setCurrentFilters(newFilters);
|
|
64
|
+
if (newFilters[0]?.value === oldFilters?.[0]?.value) return;
|
|
117
65
|
currentSorting.value = "Sortieren";
|
|
66
|
+
filterChain = filterChain
|
|
67
|
+
.catch(() => {})
|
|
68
|
+
.then(() => setFilters(newFilters))
|
|
69
|
+
.catch(() => {});
|
|
118
70
|
});
|
|
119
71
|
|
|
120
|
-
watch(currentSorting, async () => {
|
|
72
|
+
watch(currentSorting, async (val) => {
|
|
73
|
+
if (val === currentSortingOrder.value) return;
|
|
121
74
|
const sortingQuery = {
|
|
122
|
-
query:
|
|
123
|
-
properties:
|
|
75
|
+
query: currentFilters.value?.search,
|
|
76
|
+
properties: currentFilters.value?.properties?.join("|"),
|
|
124
77
|
};
|
|
125
|
-
await
|
|
78
|
+
await changeSorting(val as string, sortingQuery);
|
|
126
79
|
});
|
|
127
80
|
|
|
128
|
-
async function handleFilterRest() {
|
|
129
|
-
await resetFilters();
|
|
130
|
-
}
|
|
131
|
-
|
|
132
81
|
const moreThanOneFilterAndOption = computed<boolean>(
|
|
133
82
|
() => propertyFilters.value.length > 0,
|
|
134
83
|
);
|
|
@@ -148,15 +97,12 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
148
97
|
<Breadcrumb :category-id="category?.id" />
|
|
149
98
|
<CategoryHeader v-if="category" :category="category" />
|
|
150
99
|
<div class="flex flex-row justify-between gap-4 mb-4">
|
|
151
|
-
<UBadge
|
|
152
|
-
variant="subtle"
|
|
153
|
-
:label="`${getElements.length} Produkte`"
|
|
154
|
-
/>
|
|
100
|
+
<UBadge variant="subtle" :label="`${elements.length} Produkte`" />
|
|
155
101
|
<USelect
|
|
156
102
|
v-model="currentSorting"
|
|
157
103
|
icon="i-lucide-arrow-down-wide-narrow"
|
|
158
104
|
value-key="key"
|
|
159
|
-
:items="
|
|
105
|
+
:items="sortingOrders"
|
|
160
106
|
placeholder="Sortierung"
|
|
161
107
|
/>
|
|
162
108
|
<ClientOnly v-if="moreThanOneFilterAndOption">
|
|
@@ -205,7 +151,7 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
205
151
|
label="Zurücksetzen"
|
|
206
152
|
variant="outline"
|
|
207
153
|
block
|
|
208
|
-
@click="
|
|
154
|
+
@click="resetFilters"
|
|
209
155
|
/>
|
|
210
156
|
</div>
|
|
211
157
|
</template>
|
|
@@ -231,7 +177,7 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
231
177
|
:class="{ 'opacity-40 pointer-events-none': loading }"
|
|
232
178
|
>
|
|
233
179
|
<ProductCard
|
|
234
|
-
v-for="product in
|
|
180
|
+
v-for="product in elements"
|
|
235
181
|
:key="product.id"
|
|
236
182
|
:product="product"
|
|
237
183
|
:with-favorite-button="true"
|
|
@@ -281,7 +227,7 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
281
227
|
label="Zurücksetzen"
|
|
282
228
|
variant="outline"
|
|
283
229
|
block
|
|
284
|
-
@click="
|
|
230
|
+
@click="resetFilters"
|
|
285
231
|
/>
|
|
286
232
|
</div>
|
|
287
233
|
<template #fallback>
|
|
@@ -14,17 +14,12 @@ const { variants: selectableOptions } = useProductVariantsZwei(configurator);
|
|
|
14
14
|
|
|
15
15
|
const selectedOptions = ref<Record<string, string>>({});
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
selectedOptions.value[option.group.id] = option.id;
|
|
22
|
-
}
|
|
17
|
+
const options = product.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;
|
|
23
21
|
}
|
|
24
22
|
}
|
|
25
|
-
onMounted(() => {
|
|
26
|
-
initialOptions(product);
|
|
27
|
-
});
|
|
28
23
|
|
|
29
24
|
watch(
|
|
30
25
|
selectedOptions.value,
|
|
@@ -1,69 +1,26 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Schemas } from "#shopware";
|
|
3
|
-
import type {
|
|
4
|
-
AssociationItemProduct,
|
|
5
|
-
AssociationItem,
|
|
6
|
-
} from "~/types/Association";
|
|
7
|
-
import { useShopwareContext } from "#imports";
|
|
3
|
+
import type { AssociationItemProduct } from "~/types/Association";
|
|
8
4
|
|
|
9
|
-
const DEFAULT_CURRENCY = "EUR";
|
|
10
|
-
const DEFAULT_LOCALE = "de-DE";
|
|
11
5
|
const LOADING_ICON = "i-lucide-loader";
|
|
12
|
-
const DEFAULT_ICON = "i-lucide-plus";
|
|
13
6
|
|
|
14
7
|
const props = defineProps<{
|
|
15
8
|
product: Schemas["Product"];
|
|
16
9
|
}>();
|
|
17
10
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const { getFormattedPrice } = usePrice({
|
|
22
|
-
currencyCode: DEFAULT_CURRENCY,
|
|
23
|
-
localeCode: DEFAULT_LOCALE,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
function mapAssociationToItems(
|
|
27
|
-
associations: Schemas["CrossSellingElement"][],
|
|
28
|
-
): AssociationItem[] {
|
|
29
|
-
return associations.map((crossSellingElement) => ({
|
|
30
|
-
label: crossSellingElement.crossSelling.name,
|
|
31
|
-
products: crossSellingElement.products.map(
|
|
32
|
-
(product: Schemas["Product"]) => ({
|
|
33
|
-
label: product.name,
|
|
34
|
-
value: product.id,
|
|
35
|
-
price: getFormattedPrice(product.calculatedPrice.unitPrice),
|
|
36
|
-
icon: DEFAULT_ICON,
|
|
37
|
-
}),
|
|
38
|
-
),
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const { apiClient } = useShopwareContext();
|
|
43
|
-
|
|
44
|
-
const { data: productAssociations, pending: isAssociationsLoading } =
|
|
45
|
-
useAsyncData(`cross-selling-${product.value.id}`, async () => {
|
|
46
|
-
const response = await apiClient.invoke(
|
|
47
|
-
"readProductCrossSellings post /product/{productId}/cross-selling",
|
|
48
|
-
{
|
|
49
|
-
pathParams: { productId: product.value.id },
|
|
50
|
-
},
|
|
51
|
-
);
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
"extras-selected": [selectedExtras: AssociationItemProduct[]];
|
|
13
|
+
}>();
|
|
52
14
|
|
|
53
|
-
|
|
54
|
-
});
|
|
15
|
+
const selectedExtras = ref<AssociationItemProduct[]>([]);
|
|
55
16
|
|
|
56
|
-
const associationItems =
|
|
57
|
-
|
|
17
|
+
const { associationItems, isAssociationsLoading } = useProductCrossSelling(
|
|
18
|
+
() => props.product.id,
|
|
58
19
|
);
|
|
59
20
|
|
|
60
21
|
watch(selectedExtras, () => emit("extras-selected", selectedExtras.value), {
|
|
61
22
|
deep: true,
|
|
62
23
|
});
|
|
63
|
-
|
|
64
|
-
const emit = defineEmits<{
|
|
65
|
-
"extras-selected": [selectedExtras: AssociationItemProduct[]];
|
|
66
|
-
}>();
|
|
67
24
|
</script>
|
|
68
25
|
|
|
69
26
|
<template>
|
|
@@ -1,142 +1,30 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type {
|
|
3
|
-
import type { AssociationItemProduct } from "~/types/Association";
|
|
4
|
-
import { useTrackEvent } from "~/composables/useTrackEvent";
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
5
3
|
|
|
6
4
|
const props = defineProps<{
|
|
7
5
|
productId: string;
|
|
8
6
|
}>();
|
|
9
7
|
|
|
10
|
-
const
|
|
11
|
-
const { trackProductView } = useTrackEvent();
|
|
12
|
-
|
|
13
|
-
const productId = toRef(props.productId);
|
|
14
|
-
|
|
15
|
-
const searchCriteria = {
|
|
16
|
-
includes: {
|
|
17
|
-
product: [
|
|
18
|
-
"id",
|
|
19
|
-
"productNumber",
|
|
20
|
-
"name",
|
|
21
|
-
"description",
|
|
22
|
-
"calculatedPrice",
|
|
23
|
-
"translated",
|
|
24
|
-
"properties",
|
|
25
|
-
"propertyIds",
|
|
26
|
-
"options",
|
|
27
|
-
"optionIds",
|
|
28
|
-
"seoCategory",
|
|
29
|
-
"configuratorSettings",
|
|
30
|
-
"children",
|
|
31
|
-
"parentId",
|
|
32
|
-
"sortedProperties",
|
|
33
|
-
"cover",
|
|
34
|
-
],
|
|
35
|
-
property: ["id", "name", "translated", "options"],
|
|
36
|
-
property_group_option: ["id", "name", "translated", "group"],
|
|
37
|
-
product_configurator_setting: ["id", "optionId", "option", "productId"],
|
|
38
|
-
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
39
|
-
category: ["id", "name", "translated"],
|
|
40
|
-
},
|
|
41
|
-
associations: {
|
|
42
|
-
cover: {
|
|
43
|
-
associations: {
|
|
44
|
-
media: {},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
properties: {
|
|
48
|
-
associations: {
|
|
49
|
-
group: {},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
options: {
|
|
53
|
-
associations: {
|
|
54
|
-
group: {},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
configuratorSettings: {
|
|
58
|
-
associations: {
|
|
59
|
-
option: {
|
|
60
|
-
associations: {
|
|
61
|
-
group: {},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
children: {
|
|
67
|
-
associations: {
|
|
68
|
-
properties: {
|
|
69
|
-
associations: {
|
|
70
|
-
group: {},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
options: {
|
|
74
|
-
associations: {
|
|
75
|
-
group: {},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
} as operations["searchPage post /search"]["body"];
|
|
82
|
-
|
|
83
|
-
const { data: productDetails, pending } = useAsyncData(
|
|
84
|
-
() => `product-${productId.value ?? "none"}`,
|
|
85
|
-
async () => {
|
|
86
|
-
if (!productId.value) return null;
|
|
87
|
-
const response = await apiClient.invoke(
|
|
88
|
-
"readProductDetail post /product/{productId}",
|
|
89
|
-
{
|
|
90
|
-
pathParams: { productId: productId.value },
|
|
91
|
-
body: searchCriteria,
|
|
92
|
-
},
|
|
93
|
-
);
|
|
94
|
-
return response.data;
|
|
95
|
-
},
|
|
96
|
-
);
|
|
8
|
+
const emit = defineEmits(["product-added", "variant-selected"]);
|
|
97
9
|
|
|
98
10
|
const {
|
|
11
|
+
productDetails,
|
|
12
|
+
pending,
|
|
99
13
|
selectedProduct,
|
|
100
14
|
selectedQuantity,
|
|
101
15
|
isLoading,
|
|
102
16
|
addToCart,
|
|
103
17
|
setSelectedProduct,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} =
|
|
107
|
-
|
|
108
|
-
// Initialize when productDetails is loaded
|
|
109
|
-
watch(
|
|
110
|
-
() => productDetails.value?.product,
|
|
111
|
-
(product) => {
|
|
112
|
-
if (product) {
|
|
113
|
-
setSelectedProduct(product);
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
{ immediate: true },
|
|
117
|
-
);
|
|
18
|
+
onExtrasSelected,
|
|
19
|
+
onIngredientsDeselected,
|
|
20
|
+
} = useProductDetail(() => props.productId);
|
|
118
21
|
|
|
119
22
|
const onVariantSwitched = (variant: Schemas["Product"]) => {
|
|
120
23
|
setSelectedProduct(variant);
|
|
121
24
|
emit("variant-selected", variant);
|
|
122
25
|
};
|
|
123
26
|
|
|
124
|
-
const onExtrasSelected = (extras: AssociationItemProduct[]) => {
|
|
125
|
-
setSelectedExtras(extras);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const onIngredientsDeselected = (deselected: string[]) => {
|
|
129
|
-
setDeselectedIngredients(deselected);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
27
|
const onAddToCart = () => emit("product-added");
|
|
133
|
-
|
|
134
|
-
const emit = defineEmits(["product-added", "variant-selected"]);
|
|
135
|
-
|
|
136
|
-
watch(productDetails, () => {
|
|
137
|
-
if (!productDetails.value) return;
|
|
138
|
-
trackProductView(productDetails.value.product);
|
|
139
|
-
});
|
|
140
28
|
</script>
|
|
141
29
|
|
|
142
30
|
<template>
|
|
@@ -1,38 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Schemas } from "#shopware";
|
|
2
|
+
import type { NitroFetchRequest } from "nitropack";
|
|
2
3
|
|
|
3
4
|
export async function useCategory(categoryId: Ref<string>) {
|
|
4
|
-
const {
|
|
5
|
+
const { data: category } = await useFetch<Schemas["Category"]>(
|
|
6
|
+
`/api/category/${categoryId.value}` as NitroFetchRequest,
|
|
7
|
+
);
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
includes: {
|
|
8
|
-
category: [
|
|
9
|
-
"name",
|
|
10
|
-
"translated",
|
|
11
|
-
"seoUrl",
|
|
12
|
-
"externalLink",
|
|
13
|
-
"customFields",
|
|
14
|
-
],
|
|
15
|
-
},
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const cacheKey = computed(() => `category-${categoryId.value}`);
|
|
19
|
-
|
|
20
|
-
const { data } = await useAsyncData(cacheKey, async () => {
|
|
21
|
-
const response = await apiClient.invoke(
|
|
22
|
-
"readCategoryGet get /category/{navigationId}",
|
|
23
|
-
{
|
|
24
|
-
// @ts-expect-error: _criteria is not in the type definition
|
|
25
|
-
query: { _criteria: criteria },
|
|
26
|
-
pathParams: {
|
|
27
|
-
navigationId: categoryId.value,
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
return response.data;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
category: data,
|
|
37
|
-
};
|
|
9
|
+
return { category };
|
|
38
10
|
}
|