@shopbite-de/storefront 1.18.5 → 1.19.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/composables/useCategory.ts +6 -34
- package/app/composables/useCategoryListing.ts +131 -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 +6 -3
- package/package.json +1 -1
- package/server/api/category/[categoryId].get.ts +25 -0
- package/server/api/listing/[categoryId].get.ts +107 -0
|
@@ -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>
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getListingFilters } from "@shopware/helpers";
|
|
2
|
+
import type { Schemas, operations } from "#shopware";
|
|
3
|
+
import type { NitroFetchRequest } from "nitropack";
|
|
4
|
+
|
|
5
|
+
export type CategoryListingCriteria = NonNullable<
|
|
6
|
+
operations["readProductListingGet get /product-listing/{categoryId}"]["query"]
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export type ShortcutFilterParam = {
|
|
10
|
+
code: string;
|
|
11
|
+
value: string | string[] | undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CRITERIA: CategoryListingCriteria = {
|
|
15
|
+
includes: {
|
|
16
|
+
product: [
|
|
17
|
+
"id",
|
|
18
|
+
"productNumber",
|
|
19
|
+
"name",
|
|
20
|
+
"description",
|
|
21
|
+
"calculatedPrice",
|
|
22
|
+
"translated",
|
|
23
|
+
"properties",
|
|
24
|
+
"propertyIds",
|
|
25
|
+
"sortedProperties",
|
|
26
|
+
"cover",
|
|
27
|
+
],
|
|
28
|
+
property: ["id", "name", "translated", "options"],
|
|
29
|
+
property_group_option: ["id", "name", "translated", "group"],
|
|
30
|
+
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
31
|
+
},
|
|
32
|
+
associations: {
|
|
33
|
+
cover: {
|
|
34
|
+
associations: {
|
|
35
|
+
media: {},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
properties: {
|
|
39
|
+
associations: {
|
|
40
|
+
group: {},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
limit: 100,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export function useCategoryListing(
|
|
48
|
+
categoryId: string,
|
|
49
|
+
_defaultCriteria: CategoryListingCriteria = DEFAULT_CRITERIA,
|
|
50
|
+
) {
|
|
51
|
+
const nuxtApp = useNuxtApp();
|
|
52
|
+
|
|
53
|
+
async function fetchListing(
|
|
54
|
+
criteria: CategoryListingCriteria,
|
|
55
|
+
): Promise<Schemas["ProductListingResult"]> {
|
|
56
|
+
// Send only the allowlisted filter params; the server merges them with the
|
|
57
|
+
// fixed includes/associations/limit so clients cannot influence projections.
|
|
58
|
+
const c = criteria as Record<string, unknown>;
|
|
59
|
+
return await $fetch(`/api/listing/${categoryId}` as NitroFetchRequest, {
|
|
60
|
+
query: {
|
|
61
|
+
order: c.order,
|
|
62
|
+
properties: c.properties,
|
|
63
|
+
manufacturer: c.manufacturer,
|
|
64
|
+
query: c.query,
|
|
65
|
+
p: c.p,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const loading = ref(false);
|
|
71
|
+
|
|
72
|
+
const { data: listing, pending } = useLazyAsyncData(
|
|
73
|
+
`listing-${categoryId}`,
|
|
74
|
+
() => fetchListing({}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Suppress skeleton during SSR hydration to prevent hydration mismatch.
|
|
78
|
+
const showSkeleton = computed(() => pending.value && !nuxtApp.isHydrating);
|
|
79
|
+
|
|
80
|
+
const elements = computed(() => listing.value?.elements ?? []);
|
|
81
|
+
const sortingOrders = computed(() => listing.value?.availableSortings);
|
|
82
|
+
const currentSortingOrder = computed(() => listing.value?.sorting);
|
|
83
|
+
const availableFilters = computed(() =>
|
|
84
|
+
getListingFilters(listing.value?.aggregations),
|
|
85
|
+
);
|
|
86
|
+
const currentFilters = computed(() => listing.value?.currentFilters);
|
|
87
|
+
|
|
88
|
+
async function applySearch(criteria: CategoryListingCriteria) {
|
|
89
|
+
loading.value = true;
|
|
90
|
+
try {
|
|
91
|
+
listing.value = await fetchListing(criteria);
|
|
92
|
+
} finally {
|
|
93
|
+
loading.value = false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function changeSorting(order: string, query?: CategoryListingCriteria) {
|
|
98
|
+
await applySearch({ ...query, order } as CategoryListingCriteria);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function setFilters(filters: ShortcutFilterParam[]) {
|
|
102
|
+
const filterObj: Record<string, unknown> = {};
|
|
103
|
+
for (const f of filters) {
|
|
104
|
+
filterObj[f.code] = f.value;
|
|
105
|
+
}
|
|
106
|
+
const appliedFilters = {
|
|
107
|
+
query: currentFilters.value?.search,
|
|
108
|
+
manufacturer: currentFilters.value?.manufacturer?.join("|"),
|
|
109
|
+
properties: currentFilters.value?.properties?.join("|"),
|
|
110
|
+
...filterObj,
|
|
111
|
+
};
|
|
112
|
+
await applySearch(appliedFilters as CategoryListingCriteria);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function resetFilters() {
|
|
116
|
+
await applySearch({});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
showSkeleton,
|
|
121
|
+
loading,
|
|
122
|
+
elements,
|
|
123
|
+
sortingOrders,
|
|
124
|
+
currentSortingOrder,
|
|
125
|
+
availableFilters,
|
|
126
|
+
currentFilters,
|
|
127
|
+
changeSorting,
|
|
128
|
+
setFilters,
|
|
129
|
+
resetFilters,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
public: { site },
|
|
4
|
+
} = useRuntimeConfig();
|
|
5
|
+
|
|
6
|
+
useSeoMeta({
|
|
7
|
+
title: `Prüfen & Bestellen | ${site.name}`,
|
|
8
|
+
robots: "noindex, nofollow",
|
|
9
|
+
});
|
|
10
|
+
|
|
2
11
|
const { isLoggedIn, isGuestSession } = useUser();
|
|
3
12
|
const { setStep } = useCheckoutStore();
|
|
4
13
|
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
public: { site },
|
|
4
|
+
} = useRuntimeConfig();
|
|
5
|
+
|
|
6
|
+
useSeoMeta({
|
|
7
|
+
title: `Warenkorb | ${site.name}`,
|
|
8
|
+
robots: "noindex, nofollow",
|
|
9
|
+
});
|
|
10
|
+
|
|
2
11
|
const { isLoggedIn, isGuestSession } = useUser();
|
|
3
12
|
const { isEmpty } = useCart();
|
|
4
13
|
const { setStep } = useCheckoutStore();
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
const {
|
|
3
|
+
public: { site },
|
|
4
|
+
} = useRuntimeConfig();
|
|
5
|
+
|
|
6
|
+
useSeoMeta({
|
|
7
|
+
title: `Zahlung & Versand | ${site.name}`,
|
|
8
|
+
robots: "noindex, nofollow",
|
|
9
|
+
});
|
|
10
|
+
|
|
2
11
|
const { isLoggedIn, isGuestSession } = useUser();
|
|
3
12
|
const { setStep } = useCheckoutStore();
|
|
4
13
|
|
package/nuxt.config.ts
CHANGED
|
@@ -72,6 +72,9 @@ export default defineNuxtConfig({
|
|
|
72
72
|
},
|
|
73
73
|
|
|
74
74
|
routeRules: {
|
|
75
|
+
"/": {
|
|
76
|
+
prerender: true,
|
|
77
|
+
},
|
|
75
78
|
"/merkliste": {
|
|
76
79
|
ssr: false,
|
|
77
80
|
},
|
|
@@ -110,9 +113,7 @@ export default defineNuxtConfig({
|
|
|
110
113
|
experimental: { sqliteConnector: "native" },
|
|
111
114
|
},
|
|
112
115
|
|
|
113
|
-
vitalizer: {
|
|
114
|
-
disablePrefetchLinks: true,
|
|
115
|
-
},
|
|
116
|
+
vitalizer: {},
|
|
116
117
|
|
|
117
118
|
pwa: {
|
|
118
119
|
strategies: sw ? "injectManifest" : "generateSW",
|
|
@@ -123,6 +124,8 @@ export default defineNuxtConfig({
|
|
|
123
124
|
name: storeName,
|
|
124
125
|
short_name: storeName,
|
|
125
126
|
theme_color: "#ff5b00",
|
|
127
|
+
display: "standalone",
|
|
128
|
+
start_url: "/",
|
|
126
129
|
icons: [
|
|
127
130
|
{
|
|
128
131
|
src: "logo-192.png",
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { encodeForQuery } from "@shopware/api-client/helpers";
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
|
|
4
|
+
const criteria = encodeForQuery({
|
|
5
|
+
includes: {
|
|
6
|
+
category: ["name", "translated", "seoUrl", "externalLink", "customFields"],
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export default defineCachedEventHandler(
|
|
11
|
+
async (event): Promise<Schemas["Category"]> => {
|
|
12
|
+
const categoryId = getRouterParam(event, "categoryId")!;
|
|
13
|
+
const { endpoint, accessToken } = useRuntimeConfig().public.shopware;
|
|
14
|
+
|
|
15
|
+
return await $fetch(`${endpoint}/category/${categoryId}`, {
|
|
16
|
+
headers: { "sw-access-key": accessToken },
|
|
17
|
+
query: { _criteria: criteria },
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
maxAge: 86400,
|
|
22
|
+
name: "category",
|
|
23
|
+
getKey: (event) => `category-${getRouterParam(event, "categoryId")}`,
|
|
24
|
+
},
|
|
25
|
+
);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { encodeForQuery } from "@shopware/api-client/helpers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fixed projections applied to every listing request. Defined server-side so
|
|
5
|
+
* clients cannot override includes/associations/limit via query params.
|
|
6
|
+
*/
|
|
7
|
+
const BASE_CRITERIA = {
|
|
8
|
+
includes: {
|
|
9
|
+
product: [
|
|
10
|
+
"id",
|
|
11
|
+
"productNumber",
|
|
12
|
+
"name",
|
|
13
|
+
"description",
|
|
14
|
+
"calculatedPrice",
|
|
15
|
+
"translated",
|
|
16
|
+
"properties",
|
|
17
|
+
"propertyIds",
|
|
18
|
+
"sortedProperties",
|
|
19
|
+
"cover",
|
|
20
|
+
],
|
|
21
|
+
property: ["id", "name", "translated", "options"],
|
|
22
|
+
property_group_option: ["id", "name", "translated", "group"],
|
|
23
|
+
product_option: ["id", "groupId", "name", "translated", "group"],
|
|
24
|
+
},
|
|
25
|
+
associations: {
|
|
26
|
+
cover: {
|
|
27
|
+
associations: {
|
|
28
|
+
media: {},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
properties: {
|
|
32
|
+
associations: {
|
|
33
|
+
group: {},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
limit: 100,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Validates and returns a non-empty string up to maxLength, or undefined. */
|
|
41
|
+
function sanitizeString(value: unknown, maxLength: number): string | undefined {
|
|
42
|
+
if (typeof value !== "string" || value === "") return undefined;
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
return trimmed.length <= maxLength ? trimmed : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Validates page number: positive integer in [1, 1000]. */
|
|
48
|
+
function sanitizePage(value: unknown): number | undefined {
|
|
49
|
+
const n = Number(value);
|
|
50
|
+
return Number.isInteger(n) && n >= 1 && n <= 1000 ? n : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveAllowedParams(query: Record<string, unknown>) {
|
|
54
|
+
return {
|
|
55
|
+
order: sanitizeString(query.order, 64),
|
|
56
|
+
properties: sanitizeString(query.properties, 2048),
|
|
57
|
+
manufacturer: sanitizeString(query.manufacturer, 2048),
|
|
58
|
+
query: sanitizeString(query.query, 256),
|
|
59
|
+
p: sanitizePage(query.p),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default defineCachedEventHandler(
|
|
64
|
+
async (event) => {
|
|
65
|
+
const categoryId = getRouterParam(event, "categoryId")!;
|
|
66
|
+
const { endpoint, accessToken } = useRuntimeConfig().public.shopware;
|
|
67
|
+
|
|
68
|
+
const { order, properties, manufacturer, query, p } = resolveAllowedParams(
|
|
69
|
+
getQuery(event) as Record<string, unknown>,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const criteria = {
|
|
73
|
+
...BASE_CRITERIA,
|
|
74
|
+
...(order !== undefined && { order }),
|
|
75
|
+
...(properties !== undefined && { properties }),
|
|
76
|
+
...(manufacturer !== undefined && { manufacturer }),
|
|
77
|
+
...(query !== undefined && { query }),
|
|
78
|
+
...(p !== undefined && { p }),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return await $fetch(`${endpoint}/product-listing/${categoryId}`, {
|
|
82
|
+
headers: {
|
|
83
|
+
"sw-access-key": accessToken,
|
|
84
|
+
"sw-include-seo-urls": "true",
|
|
85
|
+
},
|
|
86
|
+
query: { _criteria: encodeForQuery(criteria) },
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
maxAge: 86400,
|
|
91
|
+
name: "listing",
|
|
92
|
+
getKey: (event) => {
|
|
93
|
+
const categoryId = getRouterParam(event, "categoryId") ?? "";
|
|
94
|
+
const q = getQuery(event) as Record<string, unknown>;
|
|
95
|
+
const { order, properties, manufacturer, query, p } =
|
|
96
|
+
resolveAllowedParams(q);
|
|
97
|
+
return [
|
|
98
|
+
categoryId,
|
|
99
|
+
order ?? "",
|
|
100
|
+
properties ?? "",
|
|
101
|
+
manufacturer ?? "",
|
|
102
|
+
query ?? "",
|
|
103
|
+
p ?? 1,
|
|
104
|
+
].join("|");
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
);
|