@scayle/storefront-product-detail 1.7.0 → 1.9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @scayle/storefront-product-detail
2
2
 
3
+ ## 1.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **[RPC]** Updated built-in RPC methods to use `GET` for safe, cacheable reads and `PUT`/`POST`/`DELETE` for mutations, enabling CDN and browser caching for public data fetches.
8
+
9
+ ## 1.8.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Added the `useSimilarProducts` composable and `getSimilarProducts` RPC method to enable fetching product recommendations based on a given product ID.
14
+
15
+ The `useSimilarProducts` composable provides a convenient way to retrieve similar products with support for filtering, limiting results, and configuring product data inclusion. The underlying `getSimilarProducts` RPC method accepts a `productId` and optional configuration parameters including `with` (product data to include), `limit` (maximum number of recommendations), `where` (product search filters), `pricePromotionKey`, `campaignKey`, and `ignoreSameMasterKey` (whether to exclude products with the same master key). Results are cached for 5 minutes to improve performance.
16
+
17
+ ```ts
18
+ const { data, status, error } = useSimilarProducts({
19
+ params: {
20
+ productId: 123,
21
+ limit: 10,
22
+ with: { attributes: "all", images: { attributes: "all" } },
23
+ where: {
24
+ attributes: [
25
+ {
26
+ type: "attributes",
27
+ key: "brand",
28
+ values: [456],
29
+ },
30
+ ],
31
+ },
32
+ ignoreSameMasterKey: true,
33
+ },
34
+ });
35
+ ```
36
+
3
37
  ## 1.7.0
4
38
 
5
39
  ### Minor Changes
@@ -44,64 +78,64 @@
44
78
  **`sellableTimeframeStartsInFuture`:**
45
79
 
46
80
  ```ts
47
- import { sellableTimeframeStartsInFuture } from '@scayle/storefront-product-detail'
81
+ import { sellableTimeframeStartsInFuture } from "@scayle/storefront-product-detail";
48
82
 
49
83
  // Example timeframe
50
84
  const timeframe = {
51
- sellableFrom: '2099-01-01T00:00:00Z',
52
- sellableTo: '2100-01-01T00:00:00Z',
53
- }
85
+ sellableFrom: "2099-01-01T00:00:00Z",
86
+ sellableTo: "2100-01-01T00:00:00Z",
87
+ };
54
88
 
55
- const inFuture = sellableTimeframeStartsInFuture(timeframe)
56
- console.log(inFuture) // true if current date is before 2099-01-01
89
+ const inFuture = sellableTimeframeStartsInFuture(timeframe);
90
+ console.log(inFuture); // true if current date is before 2099-01-01
57
91
  ```
58
92
 
59
93
  **`sellableTimeframeEndsInPast`:**
60
94
 
61
95
  ```ts
62
- import { sellableTimeframeEndsInPast } from '@scayle/storefront-product-detail'
96
+ import { sellableTimeframeEndsInPast } from "@scayle/storefront-product-detail";
63
97
 
64
98
  const timeframe = {
65
- sellableFrom: '2018-01-01T00:00:00Z',
66
- sellableTo: '2020-01-01T00:00:00Z',
67
- }
99
+ sellableFrom: "2018-01-01T00:00:00Z",
100
+ sellableTo: "2020-01-01T00:00:00Z",
101
+ };
68
102
 
69
- const ended = sellableTimeframeEndsInPast(timeframe)
70
- console.log(ended) // true if current date is after 2020-01-01
103
+ const ended = sellableTimeframeEndsInPast(timeframe);
104
+ console.log(ended); // true if current date is after 2020-01-01
71
105
  ```
72
106
 
73
107
  **`isInSellableTimeframe`:**
74
108
 
75
109
  ```ts
76
- import { isInSellableTimeframe } from '@scayle/storefront-product-detail'
110
+ import { isInSellableTimeframe } from "@scayle/storefront-product-detail";
77
111
 
78
112
  const timeframe = {
79
- sellableFrom: '2023-01-01T00:00:00Z',
80
- sellableTo: '2025-01-01T00:00:00Z',
81
- }
113
+ sellableFrom: "2023-01-01T00:00:00Z",
114
+ sellableTo: "2025-01-01T00:00:00Z",
115
+ };
82
116
 
83
- const active = isInSellableTimeframe(timeframe)
84
- console.log(active) // true if current date is between 2023-01-01 and 2025-01-01
117
+ const active = isInSellableTimeframe(timeframe);
118
+ console.log(active); // true if current date is between 2023-01-01 and 2025-01-01
85
119
  ```
86
120
 
87
121
  **`useSellableTimeFrame`:**
88
122
 
89
123
  ```ts
90
- import { useSellableTimeFrame } from '@scayle/storefront-product-detail'
124
+ import { useSellableTimeFrame } from "@scayle/storefront-product-detail";
91
125
 
92
126
  const timeframe = {
93
- sellableFrom: '2023-01-01T00:00:00Z',
94
- sellableTo: '2025-01-01T00:00:00Z',
95
- }
127
+ sellableFrom: "2023-01-01T00:00:00Z",
128
+ sellableTo: "2025-01-01T00:00:00Z",
129
+ };
96
130
  const {
97
131
  isInSellableTimeframe,
98
132
  sellableTimeframeStartsInFuture,
99
133
  sellableTimeframeEndsInPast,
100
- } = useSellableTimeFrame(timeframe)
134
+ } = useSellableTimeFrame(timeframe);
101
135
 
102
- console.log(isInSellableTimeframe) // true if the timeframe is active
103
- console.log(sellableTimeframeStartsInFuture) // true if the time frame starts in the future
104
- console.log(sellableTimeframeEndsInPast) // true if the timeframe ended in the past
136
+ console.log(isInSellableTimeframe); // true if the timeframe is active
137
+ console.log(sellableTimeframeStartsInFuture); // true if the time frame starts in the future
138
+ console.log(sellableTimeframeEndsInPast); // true if the timeframe ended in the past
105
139
  ```
106
140
 
107
141
  ## 1.5.6
@@ -117,20 +151,20 @@
117
151
 
118
152
  ```ts
119
153
  // Before
120
- const { data } = await useRpc('getProduct', 'my-static-product-key', {
154
+ const { data } = await useRpc("getProduct", "my-static-product-key", {
121
155
  productId: 123,
122
- })
156
+ });
123
157
 
124
158
  // After
125
- const productId = ref(123)
159
+ const productId = ref(123);
126
160
 
127
161
  // The key is now reactive. If `productId` changes, the key updates
128
162
  // to 'product-456' (for example) and triggers a new fetch.
129
163
  const { data } = await useRpc(
130
- 'getProduct',
164
+ "getProduct",
131
165
  () => `product-${productId.value}`,
132
- { productId },
133
- )
166
+ { productId }
167
+ );
134
168
  ```
135
169
 
136
170
  ## 1.5.5
@@ -167,15 +201,15 @@
167
201
 
168
202
  ```ts
169
203
  export default defineNuxtConfig({
170
- modules: ['@scayle/storefront-product-detail'],
204
+ modules: ["@scayle/storefront-product-detail"],
171
205
  runtimeConfig: {
172
206
  public: {
173
- 'product-detail': {
207
+ "product-detail": {
174
208
  maxRecentlyViewedProducts: 15,
175
209
  },
176
210
  },
177
211
  },
178
- })
212
+ });
179
213
  ```
180
214
 
181
215
  ## 1.5.0
@@ -187,15 +221,15 @@
187
221
  Example usage:
188
222
 
189
223
  ```ts
190
- import { useRecentlyViewedProducts } from '#storefront-product-detail/composables'
224
+ import { useRecentlyViewedProducts } from "#storefront-product-detail/composables";
191
225
 
192
226
  const { products, addProduct, loadMissingProducts } =
193
- useRecentlyViewedProducts()
227
+ useRecentlyViewedProducts();
194
228
 
195
229
  onBeforeMount(async () => {
196
- addProductId(1234)
197
- await loadMissingProducts()
198
- })
230
+ addProductId(1234);
231
+ await loadMissingProducts();
232
+ });
199
233
  ```
200
234
 
201
235
  ## 1.4.2
@@ -227,46 +261,44 @@
227
261
 
228
262
  ```ts
229
263
  useProductSeoData({
230
- name: 'Name',
231
- brand: 'Brand',
232
- productDescription: 'Description',
264
+ name: "Name",
265
+ brand: "Brand",
266
+ productDescription: "Description",
233
267
  variants: variants.value.map((variant) => {
234
268
  return generateProductSchema({
235
- productName: 'Name',
269
+ productName: "Name",
236
270
  variant,
237
271
  url: `${$config.public.baseUrl}${route.fullPath}`,
238
272
  size,
239
- })
273
+ });
240
274
  }),
241
275
  images: images.value,
242
276
  productId: product.value?.id || 0,
243
277
  color: formatColors(colors.value),
244
- variesBy: variants.value.length > 1
245
- ? ['https://schema.org/size']
246
- : undefined,
247
- })
278
+ variesBy:
279
+ variants.value.length > 1 ? ["https://schema.org/size"] : undefined,
280
+ });
248
281
 
249
282
  // Will become
250
283
 
251
284
  useProductSeoData({
252
- name: 'Name',
253
- brand: 'Brand',
254
- productDescription: 'Description',
285
+ name: "Name",
286
+ brand: "Brand",
287
+ productDescription: "Description",
255
288
  variants: variants.value.map((variant) => {
256
289
  return generateProductSchema({
257
- productName: 'Name',
290
+ productName: "Name",
258
291
  variant,
259
292
  url: `${$config.public.baseUrl}${route.fullPath}`,
260
293
  size,
261
294
  images: images.value[0],
262
- })
295
+ });
263
296
  }),
264
297
  productId: product.value?.id || 0,
265
298
  color: formatColors(colors.value),
266
- variesBy: variants.value.length > 1
267
- ? ['https://schema.org/size']
268
- : undefined,
269
- })
299
+ variesBy:
300
+ variants.value.length > 1 ? ["https://schema.org/size"] : undefined,
301
+ });
270
302
  ```
271
303
 
272
304
  ## 1.2.0
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
3
  "configKey": "product-detail",
4
- "version": "1.7.0",
4
+ "version": "1.9.0",
5
5
  "compatibility": {
6
6
  "bridge": false,
7
7
  "nuxt": ">=3.13"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { defineNuxtModule, createResolver, addImportsDir } from '@nuxt/kit';
2
2
 
3
3
  const PACKAGE_NAME = "@scayle/storefront-product-detail";
4
- const PACKAGE_VERSION = "1.7.0";
4
+ const PACKAGE_VERSION = "1.9.0";
5
5
  const module$1 = defineNuxtModule({
6
6
  meta: {
7
7
  name: PACKAGE_NAME,
@@ -24,6 +24,10 @@ const module$1 = defineNuxtModule({
24
24
  source: await resolvePath("./runtime/rpc/methods/products"),
25
25
  names: ["getAllShopProductsForId"]
26
26
  });
27
+ customRpcs.push({
28
+ source: await resolvePath("./runtime/rpc/methods/recommendations"),
29
+ names: ["getSimilarProducts"]
30
+ });
27
31
  });
28
32
  nuxt.options.optimization.keyedComposables.push({
29
33
  name: "useAllShopProductsForId",
@@ -2,3 +2,4 @@ export * from './useProductSeoData.js';
2
2
  export * from './useAllShopProductsForId.js';
3
3
  export * from './useRecentlyViewedProducts.js';
4
4
  export * from './useSellableTimeFrame.js';
5
+ export * from './useSimilarProducts.js';
@@ -2,3 +2,4 @@ export * from "./useProductSeoData.js";
2
2
  export * from "./useAllShopProductsForId.js";
3
3
  export * from "./useRecentlyViewedProducts.js";
4
4
  export * from "./useSellableTimeFrame.js";
5
+ export * from "./useSimilarProducts.js";
@@ -0,0 +1,26 @@
1
+ import type { RpcMethodParameters } from '@scayle/storefront-nuxt';
2
+ import type { NormalizedRpcResponse, UseRpcOptions, UseRpcCacheKey, KeysOf, UseRpcReturn } from '@scayle/storefront-nuxt/composables';
3
+ import type { MaybeRefOrGetter } from 'vue';
4
+ /**
5
+ * Retrieves similar products for a given product ID using the `getSimilarProducts` RPC method.
6
+ *
7
+ * This function acts as a wrapper around the `useRpc` composable,
8
+ * simplifying the process of fetching data. It internally calls `useRpc`
9
+ * with the provided parameters and options.
10
+ *
11
+ * @template DataT The type of the normalized RPC response data. Defaults to `NormalizedRpcResponse<'getSimilarProducts'>`.
12
+ * @template PickKeys The keys to pick from the data. Defaults to all keys of `DataT`.
13
+ * @template DefaultT The default value to use if data is not available. Defaults to `null`.
14
+ *
15
+ * @param params The parameters for the `getSimilarProducts` RPC method.
16
+ * @param params.params The parameters for the `getSimilarProducts` RPC method.
17
+ * @param params.options The options for the underlying `useRpc` call, controlling data handling and loading state.
18
+ * @param key A unique key for this RPC call. Used internally by `useRpc` for caching and state management.
19
+ *
20
+ * @returns The result of the `useRpc` call, which includes the fetched data,
21
+ * loading state, and any error information.
22
+ */
23
+ export declare function useSimilarProducts<DataT = NormalizedRpcResponse<'getSimilarProducts'>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>, DefaultT = null>({ params, options, }?: Partial<{
24
+ params: MaybeRefOrGetter<RpcMethodParameters<'getSimilarProducts'>>;
25
+ options: UseRpcOptions<'getSimilarProducts', DataT, PickKeys, DefaultT>;
26
+ }>, key?: UseRpcCacheKey): UseRpcReturn<'getSimilarProducts', DataT, PickKeys, DefaultT>;
@@ -0,0 +1,7 @@
1
+ import { useRpc } from "@scayle/storefront-nuxt/composables";
2
+ export function useSimilarProducts({
3
+ params,
4
+ options
5
+ } = {}, key = "useSimilarProducts") {
6
+ return useRpc("getSimilarProducts", key, params, options);
7
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './composables/useProductSeoData.js';
2
2
  export * from './composables/useAllShopProductsForId.js';
3
3
  export * from './composables/useSellableTimeFrame.js';
4
+ export * from './composables/useSimilarProducts.js';
4
5
  export * from './utils/product.js';
5
6
  export * from './utils/attribute.js';
6
7
  export * from './utils/seo.js';
@@ -1,6 +1,7 @@
1
1
  export * from "./composables/useProductSeoData.js";
2
2
  export * from "./composables/useAllShopProductsForId.js";
3
3
  export * from "./composables/useSellableTimeFrame.js";
4
+ export * from "./composables/useSimilarProducts.js";
4
5
  export * from "./utils/product.js";
5
6
  export * from "./utils/attribute.js";
6
7
  export * from "./utils/seo.js";
@@ -30,4 +30,4 @@ export const getAllShopProductsForId = defineRpcHandler(async (options, context)
30
30
  // 12 hours
31
31
  })();
32
32
  return result;
33
- });
33
+ }, { method: "GET" });
@@ -0,0 +1,26 @@
1
+ import type { ProductWith, ProductSearchQuery, RpcHandler, Product } from '@scayle/storefront-nuxt';
2
+ interface FetchSimilarProductsParams {
3
+ productId: number;
4
+ with?: ProductWith;
5
+ pricePromotionKey?: string;
6
+ where?: ProductSearchQuery;
7
+ limit?: number;
8
+ ignoreSameMasterKey?: boolean;
9
+ }
10
+ /**
11
+ * Retrieves similar products for a given product ID.
12
+ *
13
+ * @param options The options for retrieving similar products.
14
+ * @param options.productId The ID of the product to retrieve similar products for.
15
+ * @param options.with The 'with' parameter for including additional data.
16
+ * @param options.pricePromotionKey The price promotion key.
17
+ * @param options.where The where clause to filter the products.
18
+ * @params options.where.attributes The attributes to filter the products by.
19
+ * @param options.limit The limit of products to retrieve.
20
+ * @param options.ignoreSameMasterKey Whether to ignore products with the same master key.
21
+ * @param context The RPC context.
22
+ *
23
+ * @returns A Promise that resolves with an array of products.
24
+ */
25
+ export declare const getSimilarProducts: RpcHandler<FetchSimilarProductsParams, Product[]>;
26
+ export {};
@@ -0,0 +1,38 @@
1
+ import {
2
+ ErrorResponse,
3
+ FetchError,
4
+ MINUTE,
5
+ MIN_WITH_PARAMS_PRODUCT,
6
+ defineRpcHandler
7
+ } from "@scayle/storefront-nuxt";
8
+ export const getSimilarProducts = defineRpcHandler(async (options, context) => {
9
+ const { sapiClient, cached, withParams } = context;
10
+ const campaignKey = await context.callRpc?.("getCampaignKey");
11
+ const { productId, with: withParameter, ...rest } = options;
12
+ const fetch = async (productId2, params) => {
13
+ try {
14
+ return sapiClient.recommendations.getSimilarProducts(productId2, params);
15
+ } catch (e) {
16
+ if (e instanceof FetchError) {
17
+ const response = e.response;
18
+ return new ErrorResponse(
19
+ response.status,
20
+ response.statusText,
21
+ "Failed to fetch similar products"
22
+ );
23
+ }
24
+ throw e;
25
+ }
26
+ };
27
+ return await cached(
28
+ fetch,
29
+ {
30
+ cacheKeyPrefix: "getSimilarProducts-products",
31
+ ttl: 5 * MINUTE
32
+ }
33
+ )(productId, {
34
+ with: withParameter ?? withParams?.product ?? MIN_WITH_PARAMS_PRODUCT,
35
+ campaignKey,
36
+ ...rest
37
+ });
38
+ }, { method: "GET" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Collection of essential composables and utilities to work with product detail",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -24,6 +24,9 @@
24
24
  "CHANGELOG.md",
25
25
  "dist"
26
26
  ],
27
+ "engines": {
28
+ "node": ">= 22.0.0"
29
+ },
27
30
  "imports": {
28
31
  "#storefront-product-detail": "./dist/runtime/index.js"
29
32
  },
@@ -37,28 +40,27 @@
37
40
  },
38
41
  "devDependencies": {
39
42
  "@arethetypeswrong/cli": "0.18.2",
40
- "@nuxt/kit": "3.20.0",
43
+ "@nuxt/kit": "^3.20.2",
41
44
  "@nuxt/module-builder": "1.0.2",
42
- "@nuxt/schema": "3.20.0",
43
- "@nuxt/test-utils": "3.23.0",
44
- "@types/node": "22.19.5",
45
- "@vitest/coverage-v8": "4.0.16",
46
- "@vueuse/core": "14.1.0",
47
- "dprint": "0.51.1",
48
- "eslint-formatter-gitlab": "7.0.1",
49
- "eslint": "9.39.2",
50
- "happy-dom": "20.1.0",
51
- "nuxt": "3.20.0",
52
- "publint": "0.3.16",
45
+ "@nuxt/schema": "^3.20.2",
46
+ "@nuxt/test-utils": "4.0.0",
47
+ "@scayle/eslint-config-storefront": "^4.8.0",
48
+ "@types/node": "22.19.17",
49
+ "@vitest/coverage-v8": "4.1.5",
50
+ "@vueuse/core": "14.2.1",
51
+ "eslint-formatter-gitlab": "7.1.0",
52
+ "eslint": "10.2.1",
53
+ "happy-dom": "20.9.0",
54
+ "nuxt": "^3.20.2",
55
+ "publint": "0.3.18",
53
56
  "schema-dts": "1.1.5",
54
57
  "typescript": "5.9.3",
55
58
  "unbuild": "3.6.1",
56
- "vitest": "4.0.16",
59
+ "vitest": "4.1.5",
57
60
  "vue-router": "4.6.4",
58
- "vue-tsc": "3.2.2",
59
- "vue": "3.5.26",
60
- "@scayle/eslint-config-storefront": "4.7.20",
61
- "@scayle/storefront-nuxt": "8.58.0",
61
+ "vue-tsc": "3.2.6",
62
+ "vue": "3.5.32",
63
+ "@scayle/storefront-nuxt": "8.61.0",
62
64
  "@scayle/vitest-config-storefront": "1.0.0"
63
65
  },
64
66
  "scripts": {
@@ -69,11 +71,6 @@
69
71
  "lint": "eslint .",
70
72
  "lint:ci": "eslint . --format gitlab",
71
73
  "lint:fix": "eslint . --fix",
72
- "format": "dprint check",
73
- "format:fix": "dprint fmt",
74
- "test": "vitest --run",
75
- "test:watch": "vitest",
76
- "test:ci": "vitest --run --coverage --reporter=default --reporter=junit --outputFile=./coverage/junit.xml",
77
74
  "typecheck": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
78
75
  "package:lint": "publint",
79
76
  "verify-packaging": "attw --pack . --profile esm-only"