@scayle/storefront-product-detail 1.4.2 → 1.5.1

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,46 @@
1
1
  # @scayle/storefront-product-detail
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Added a runtime configuration option to control how many recently viewed products are kept.
8
+
9
+ To configure the maximum number of recently viewed products, add the following to the `public` section of your `runtimeConfig` in `nuxt.config.ts`:
10
+
11
+ ```ts
12
+ export default defineNuxtConfig({
13
+ modules: ['@scayle/storefront-product-detail'],
14
+ runtimeConfig: {
15
+ public: {
16
+ 'product-detail': {
17
+ maxRecentlyViewedProducts: 15,
18
+ },
19
+ },
20
+ },
21
+ })
22
+ ```
23
+
24
+ ## 1.5.0
25
+
26
+ ### Minor Changes
27
+
28
+ - Added `useRecentlyViewedProducts` composable. This composable tracks a short history of products a customer has previously viewed using local storage. It maintains up to 10 recently viewed products and provides methods to add new products and fetch their full details.
29
+
30
+ Example usage:
31
+
32
+ ```ts
33
+ import { useRecentlyViewedProducts } from '#storefront-product-detail/composables'
34
+
35
+ const { products, addProduct, loadMissingProducts } =
36
+ useRecentlyViewedProducts()
37
+
38
+ onBeforeMount(async () => {
39
+ addProductId(1234)
40
+ await loadMissingProducts()
41
+ })
42
+ ```
43
+
3
44
  ## 1.4.2
4
45
 
5
46
  ### Patch Changes
@@ -81,7 +122,7 @@
81
122
 
82
123
  ### Patch Changes
83
124
 
84
- - Use absolute URL's inproduct breadcrumbs JSONLD returned by `useProductSeoData`
125
+ - Use absolute URL's in product breadcrumbs JSONLD returned by `useProductSeoData`
85
126
 
86
127
  ## 1.1.3
87
128
 
package/dist/module.d.mts CHANGED
@@ -3,6 +3,13 @@ import * as _nuxt_schema from '@nuxt/schema';
3
3
  interface ModuleOptions {
4
4
  autoImports?: boolean;
5
5
  }
6
+ declare module '@nuxt/schema' {
7
+ interface PublicRuntimeConfig {
8
+ 'product-detail': {
9
+ maxRecentlyViewedProducts?: number;
10
+ };
11
+ }
12
+ }
6
13
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
7
14
 
8
15
  export { _default as default };
package/dist/module.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
3
  "configKey": "product-detail",
4
- "version": "1.4.2",
4
+ "version": "1.5.1",
5
5
  "compatibility": {
6
6
  "bridge": false,
7
7
  "nuxt": ">=3.13"
8
8
  },
9
9
  "builder": {
10
- "@nuxt/module-builder": "1.0.1",
11
- "unbuild": "3.5.0"
10
+ "@nuxt/module-builder": "1.0.2",
11
+ "unbuild": "3.6.0"
12
12
  }
13
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.4.2";
4
+ const PACKAGE_VERSION = "1.5.1";
5
5
  const module = defineNuxtModule({
6
6
  meta: {
7
7
  name: PACKAGE_NAME,
@@ -17,6 +17,8 @@ const module = defineNuxtModule({
17
17
  const { resolve, resolvePath } = createResolver(import.meta.url);
18
18
  nuxt.options.alias["#storefront-product-detail"] = resolve("./runtime");
19
19
  nuxt.options.build.transpile.push("#storefront-product-detail/runtime");
20
+ nuxt.options.runtimeConfig.public["product-detail"] ??= {};
21
+ nuxt.options.runtimeConfig.public["product-detail"].maxRecentlyViewedProducts ??= 10;
20
22
  nuxt.hook("storefront:custom-rpc:extend", async (customRpcs) => {
21
23
  customRpcs.push({
22
24
  source: await resolvePath("./runtime/rpc/methods/products"),
@@ -1,2 +1,3 @@
1
1
  export * from './useProductSeoData.js';
2
2
  export * from './useAllShopProductsForId.js';
3
+ export * from './useRecentlyViewedProducts.js';
@@ -1,2 +1,3 @@
1
1
  export * from "./useProductSeoData.js";
2
2
  export * from "./useAllShopProductsForId.js";
3
+ export * from "./useRecentlyViewedProducts.js";
@@ -0,0 +1,50 @@
1
+ import type { Product, ProductWith } from '@scayle/storefront-nuxt';
2
+ import type { AsyncDataRequestStatus } from 'nuxt/app';
3
+ import type { Ref } from 'vue';
4
+ interface UseRecentlyViewedProductsReturn {
5
+ /** A reactive array of the fetched, full Product objects. */
6
+ products: Ref<Product[]>;
7
+ /** A reactive flag indicating if the product data is being fetched. */
8
+ loading: Ref<boolean>;
9
+ /** A reactive container for any errors that occur during fetching. */
10
+ error: Ref<Error | undefined>;
11
+ /** A string indicating the status of the data request */
12
+ status: Ref<AsyncDataRequestStatus>;
13
+ /**
14
+ * Adds a product id to the list of recently viewed products and persists it to local storage.
15
+ *
16
+ * Because this function uses local storage, it is not available on the server.
17
+ *
18
+ * @throws Error if the function is called on the server.
19
+ *
20
+ * @param productId - The id of the product to add to the list of recently viewed products.
21
+ */
22
+ addProductId: (productId: number) => void;
23
+ /**
24
+ * Loads products based on the product ids previously added.
25
+ * Initially all products within the persisted list are loaded. Subsequent calls only load products that are not already loaded.
26
+ *
27
+ * Because this function uses local storage, it is not available on the server.
28
+ *
29
+ * @throws Error if the function is called on the server.
30
+ *
31
+ * @returns A promise that resolves when the products have been loaded.
32
+ */
33
+ loadMissingProducts: () => Promise<void>;
34
+ }
35
+ /**
36
+ * This composable can be used to track a short history of products a customer has previously viewed using local storage.
37
+ * It maintains up to 10 recently viewed products and provides methods to add new products and fetch their full details.
38
+ *
39
+ * @param options An object containing parameters and options for the loading the products.
40
+ * @param options.with The fields to include in the response.
41
+ * @param options.pricePromotionKey The price promotion key.
42
+ *
43
+ * @returns An object containing the recently viewed products list, loading state, error and status information.
44
+ */
45
+ interface UseRecentlyViewedProductsOptions {
46
+ pricePromotionKey?: string;
47
+ with?: ProductWith;
48
+ }
49
+ export declare function useRecentlyViewedProducts(options?: UseRecentlyViewedProductsOptions): UseRecentlyViewedProductsReturn;
50
+ export {};
@@ -0,0 +1,90 @@
1
+ import { useCurrentShop, useRpcCall } from "@scayle/storefront-nuxt/composables";
2
+ import { useLocalStorage } from "@vueuse/core";
3
+ import { useState, useRuntimeConfig } from "nuxt/app";
4
+ export function useRecentlyViewedProducts(options) {
5
+ const shop = useCurrentShop();
6
+ const maxRecentlyViewedProducts = useRuntimeConfig().public["product-detail"].maxRecentlyViewedProducts ?? 10;
7
+ const recentlyViewedProductsIds = useLocalStorage(
8
+ `${shop.value.shopId}-recently-viewed-products`,
9
+ []
10
+ );
11
+ const fetchProductsRpc = useRpcCall("getProductsByIds");
12
+ const loading = useState("recently-viewed-products-loading", () => false);
13
+ const error = useState("recently-viewed-products-error");
14
+ const status = useState(
15
+ "recently-viewed-products-status",
16
+ () => "idle"
17
+ );
18
+ const products = useState("recently-viewed-products", () => []);
19
+ const addProductId = (productId) => {
20
+ if (import.meta.server) {
21
+ throw new Error(
22
+ "useRecentlyViewedProducts uses local storage. addProductId is therefore not available on the server."
23
+ );
24
+ }
25
+ const productIds = [...recentlyViewedProductsIds.value];
26
+ if (productIds.includes(productId)) {
27
+ productIds.splice(productIds.indexOf(productId), 1);
28
+ }
29
+ productIds.unshift(productId);
30
+ if (productIds.length > maxRecentlyViewedProducts) {
31
+ productIds.pop();
32
+ }
33
+ recentlyViewedProductsIds.value = productIds;
34
+ };
35
+ const loadMissingProducts = async () => {
36
+ if (import.meta.server) {
37
+ throw new Error(
38
+ "useRecentlyViewedProducts uses local storage. fetchProducts is therefore not available on the server."
39
+ );
40
+ }
41
+ loading.value = true;
42
+ status.value = "pending";
43
+ try {
44
+ const loadedProductIds = new Set(
45
+ products.value.map((product) => product.id)
46
+ );
47
+ const missingProductIds = recentlyViewedProductsIds.value.filter(
48
+ (id) => !loadedProductIds.has(id)
49
+ );
50
+ if (missingProductIds.length > 0) {
51
+ const result = await fetchProductsRpc({
52
+ ids: missingProductIds,
53
+ with: options?.with,
54
+ pricePromotionKey: options?.pricePromotionKey
55
+ });
56
+ products.value.push(...result);
57
+ }
58
+ const sortOrderMap = new Map(
59
+ recentlyViewedProductsIds.value.map(
60
+ (id, index) => [id, index]
61
+ )
62
+ );
63
+ products.value = products.value.sort((a, b) => {
64
+ const aIndex = sortOrderMap.get(a.id);
65
+ const bIndex = sortOrderMap.get(b.id);
66
+ if (aIndex === void 0) {
67
+ return 1;
68
+ }
69
+ if (bIndex === void 0) {
70
+ return -1;
71
+ }
72
+ return aIndex - bIndex;
73
+ }).slice(0, maxRecentlyViewedProducts);
74
+ status.value = "success";
75
+ } catch (e) {
76
+ error.value = e;
77
+ status.value = "error";
78
+ } finally {
79
+ loading.value = false;
80
+ }
81
+ };
82
+ return {
83
+ addProductId,
84
+ loadMissingProducts,
85
+ products,
86
+ loading,
87
+ error,
88
+ status
89
+ };
90
+ }
@@ -1,4 +1,5 @@
1
- export const getAllShopProductsForId = async function getAllShopProductsForId2(options, context) {
1
+ import { defineRpcHandler } from "@scayle/storefront-nuxt";
2
+ export const getAllShopProductsForId = defineRpcHandler(async (options, context) => {
2
3
  const { runtimeConfiguration, sapiClient, log, cached } = context;
3
4
  const fetchAllProducts = async () => {
4
5
  const products = [];
@@ -29,4 +30,4 @@ export const getAllShopProductsForId = async function getAllShopProductsForId2(o
29
30
  // 12 hours
30
31
  })();
31
32
  return result;
32
- };
33
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
- "version": "1.4.2",
3
+ "version": "1.5.1",
4
4
  "description": "Collection of essential composables and utilities to work with product detail",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -27,11 +27,44 @@
27
27
  "imports": {
28
28
  "#storefront-product-detail": "./dist/runtime/index.js"
29
29
  },
30
+ "peerDependencies": {
31
+ "@nuxt/kit": ">=3.13.0",
32
+ "@scayle/storefront-nuxt": "^8.0.0",
33
+ "@vueuse/core": "^12.8.2 || ^13.0.0",
34
+ "schema-dts": "^1.1.5",
35
+ "vue-router": "^4.5.0",
36
+ "vue": "^3.5.13"
37
+ },
38
+ "devDependencies": {
39
+ "@arethetypeswrong/cli": "0.18.2",
40
+ "@nuxt/kit": "3.17.7",
41
+ "@nuxt/module-builder": "1.0.2",
42
+ "@nuxt/schema": "3.17.7",
43
+ "@nuxt/test-utils": "3.19.2",
44
+ "@types/node": "22.16.5",
45
+ "@vitest/coverage-v8": "3.2.4",
46
+ "@vueuse/core": "13.6.0",
47
+ "dprint": "0.50.1",
48
+ "eslint-formatter-gitlab": "6.0.1",
49
+ "eslint": "9.32.0",
50
+ "happy-dom": "18.0.1",
51
+ "nuxt": "3.17.7",
52
+ "publint": "0.3.12",
53
+ "schema-dts": "1.1.5",
54
+ "typescript": "5.8.3",
55
+ "unbuild": "3.6.0",
56
+ "vitest": "3.2.4",
57
+ "vue-router": "4.5.1",
58
+ "vue-tsc": "3.0.4",
59
+ "vue": "3.5.18",
60
+ "@scayle/eslint-config-storefront": "4.7.2",
61
+ "@scayle/storefront-nuxt": "8.38.4"
62
+ },
30
63
  "scripts": {
31
64
  "build": "nuxt-module-build build",
32
- "dev": "nuxi dev playground",
33
- "dev:build": "nuxi build playground",
34
- "prep": "nuxt-module-build prepare && nuxi prepare playground",
65
+ "dev": "nuxt dev playground",
66
+ "dev:build": "nuxt build playground",
67
+ "prep": "nuxt-module-build prepare && nuxt prepare playground",
35
68
  "lint": "eslint .",
36
69
  "lint:ci": "eslint . --format gitlab",
37
70
  "lint:fix": "eslint . --fix",
@@ -43,39 +76,5 @@
43
76
  "typecheck": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
44
77
  "package:lint": "publint",
45
78
  "verify-packaging": "attw --pack . --profile esm-only"
46
- },
47
- "peerDependencies": {
48
- "@nuxt/kit": ">=3.13.0",
49
- "@scayle/storefront-nuxt": "^8.0.0",
50
- "@vue/test-utils": "^2.4.6",
51
- "@vueuse/core": "^12.8.2 || ^13.0.0",
52
- "schema-dts": "^1.1.5",
53
- "vue-router": "^4.5.0",
54
- "vue": "^3.5.13"
55
- },
56
- "devDependencies": {
57
- "@arethetypeswrong/cli": "0.18.1",
58
- "@nuxt/kit": "3.16.2",
59
- "@nuxt/module-builder": "1.0.1",
60
- "@nuxt/schema": "3.16.2",
61
- "@nuxt/test-utils": "3.18.0",
62
- "@scayle/eslint-config-storefront": "4.5.2",
63
- "@scayle/storefront-nuxt": "8.27.0",
64
- "@types/node": "22.15.21",
65
- "@vue/test-utils": "2.4.6",
66
- "@vueuse/core": "13.2.0",
67
- "dprint": "0.50.0",
68
- "eslint-formatter-gitlab": "6.0.0",
69
- "eslint": "9.27.0",
70
- "happy-dom": "17.4.7",
71
- "nuxt": "3.16.2",
72
- "publint": "0.2.12",
73
- "schema-dts": "1.1.5",
74
- "typescript": "5.8.3",
75
- "unbuild": "3.5.0",
76
- "vitest": "3.1.4",
77
- "vue-router": "4.5.1",
78
- "vue-tsc": "2.2.10",
79
- "vue": "3.5.14"
80
79
  }
81
- }
80
+ }