@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.
@@ -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 { operations, Schemas } from "#shopware";
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 searchCriteria = {
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
- resetFilters,
14
+ showSkeleton,
46
15
  loading,
47
- search,
48
- getElements,
49
- getCurrentListing,
50
- getCurrentSortingOrder,
51
- getSortingOrders,
52
- changeCurrentSortingOrder,
53
- getAvailableFilters,
54
- getCurrentFilters,
55
- setCurrentFilters,
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(getCurrentSortingOrder.value ?? "Sortieren");
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
- (getAvailableFilters.value?.filter(
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(getCurrentFilters.value?.properties ?? []);
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
- const nuxtApp = useNuxtApp();
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: getCurrentFilters.value?.search,
123
- properties: getCurrentFilters.value?.properties?.join("|"),
75
+ query: currentFilters.value?.search,
76
+ properties: currentFilters.value?.properties?.join("|"),
124
77
  };
125
- await changeCurrentSortingOrder(currentSorting.value as string, sortingQuery);
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="getSortingOrders"
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="handleFilterRest"
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 getElements"
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="handleFilterRest"
230
+ @click="resetFilters"
285
231
  />
286
232
  </div>
287
233
  <template #fallback>
@@ -1,38 +1,10 @@
1
- import { encodeForQuery } from "@shopware/api-client/helpers";
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 { apiClient } = useShopwareContext();
5
+ const { data: category } = await useFetch<Schemas["Category"]>(
6
+ `/api/category/${categoryId.value}` as NitroFetchRequest,
7
+ );
5
8
 
6
- const criteria = encodeForQuery({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopbite-de/storefront",
3
- "version": "1.18.5",
3
+ "version": "1.19.0",
4
4
  "main": "nuxt.config.ts",
5
5
  "description": "Shopware storefront for food delivery shops",
6
6
  "keywords": [
@@ -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
+ );