@scayle/storefront-product-detail 1.4.1 → 1.5.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,31 @@
1
1
  # @scayle/storefront-product-detail
2
2
 
3
+ ## 1.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 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.
8
+
9
+ Example usage:
10
+
11
+ ```ts
12
+ import { useRecentlyViewedProducts } from "#storefront-product-detail/composables";
13
+
14
+ const { products, addProduct, loadMissingProducts } =
15
+ useRecentlyViewedProducts();
16
+
17
+ onBeforeMount(async () => {
18
+ addProductId(1234);
19
+ await loadMissingProducts();
20
+ });
21
+ ```
22
+
23
+ ## 1.4.2
24
+
25
+ ### Patch Changes
26
+
27
+ - Fixed path resolution issue for RPC methods.
28
+
3
29
  ## 1.4.1
4
30
 
5
31
  ### Patch Changes
@@ -23,46 +49,44 @@
23
49
 
24
50
  ```ts
25
51
  useProductSeoData({
26
- name: 'Name',
27
- brand: 'Brand',
28
- productDescription: 'Description',
52
+ name: "Name",
53
+ brand: "Brand",
54
+ productDescription: "Description",
29
55
  variants: variants.value.map((variant) => {
30
56
  return generateProductSchema({
31
- productName: 'Name',
57
+ productName: "Name",
32
58
  variant,
33
59
  url: `${$config.public.baseUrl}${route.fullPath}`,
34
60
  size,
35
- })
61
+ });
36
62
  }),
37
63
  images: images.value,
38
64
  productId: product.value?.id || 0,
39
65
  color: formatColors(colors.value),
40
- variesBy: variants.value.length > 1
41
- ? ['https://schema.org/size']
42
- : undefined,
43
- })
66
+ variesBy:
67
+ variants.value.length > 1 ? ["https://schema.org/size"] : undefined,
68
+ });
44
69
 
45
70
  // Will become
46
71
 
47
72
  useProductSeoData({
48
- name: 'Name',
49
- brand: 'Brand',
50
- productDescription: 'Description',
73
+ name: "Name",
74
+ brand: "Brand",
75
+ productDescription: "Description",
51
76
  variants: variants.value.map((variant) => {
52
77
  return generateProductSchema({
53
- productName: 'Name',
78
+ productName: "Name",
54
79
  variant,
55
80
  url: `${$config.public.baseUrl}${route.fullPath}`,
56
81
  size,
57
82
  images: images.value[0],
58
- })
83
+ });
59
84
  }),
60
85
  productId: product.value?.id || 0,
61
86
  color: formatColors(colors.value),
62
- variesBy: variants.value.length > 1
63
- ? ['https://schema.org/size']
64
- : undefined,
65
- })
87
+ variesBy:
88
+ variants.value.length > 1 ? ["https://schema.org/size"] : undefined,
89
+ });
66
90
  ```
67
91
 
68
92
  ## 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.4.1",
4
+ "version": "1.5.0",
5
5
  "compatibility": {
6
6
  "bridge": false,
7
7
  "nuxt": ">=3.13"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { defineNuxtModule, createResolver, resolvePath, addImportsDir } from '@nuxt/kit';
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.1";
4
+ const PACKAGE_VERSION = "1.5.0";
5
5
  const module = defineNuxtModule({
6
6
  meta: {
7
7
  name: PACKAGE_NAME,
@@ -14,7 +14,7 @@ const module = defineNuxtModule({
14
14
  },
15
15
  defaults: {},
16
16
  setup(options, nuxt) {
17
- const { resolve } = createResolver(import.meta.url);
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
20
  nuxt.hook("storefront:custom-rpc:extend", async (customRpcs) => {
@@ -23,6 +23,11 @@ const module = defineNuxtModule({
23
23
  names: ["getAllShopProductsForId"]
24
24
  });
25
25
  });
26
+ nuxt.options.optimization.keyedComposables.push({
27
+ name: "useAllShopProductsForId",
28
+ argumentLength: 2,
29
+ source: "#storefront-product-detail"
30
+ });
26
31
  if (options.autoImports) {
27
32
  addImportsDir(resolve("./runtime/composables"));
28
33
  addImportsDir(resolve("./runtime/utils"));
@@ -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";
@@ -1,4 +1,7 @@
1
- import type { KeysOf, NormalizedRpcResponse, UseRpcReturn } from '@scayle/storefront-nuxt/composables';
1
+ import type { KeysOf, NormalizedRpcResponse, UseRpcOptions, UseRpcReturn } from '@scayle/storefront-nuxt/composables';
2
2
  import type { MaybeRefOrGetter } from 'vue';
3
3
  import type { RpcMethodParameters } from '@scayle/storefront-nuxt';
4
- export declare function useAllShopProductsById<DataT = NormalizedRpcResponse<'getAllShopProductsForId'>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>, DefaultT = null>(params: MaybeRefOrGetter<RpcMethodParameters<'getAllShopProductsForId'>>, key?: string): UseRpcReturn<'getAllShopProductsForId', DataT, PickKeys, DefaultT>;
4
+ export declare function useAllShopProductsForId<DataT = NormalizedRpcResponse<'getAllShopProductsForId'>, PickKeys extends KeysOf<DataT> = KeysOf<DataT>, DefaultT = null>({ params, options, }?: Partial<{
5
+ params: MaybeRefOrGetter<RpcMethodParameters<'getAllShopProductsForId'>>;
6
+ options: UseRpcOptions<'getAllShopProductsForId', DataT, PickKeys, DefaultT>;
7
+ }>, key?: string): UseRpcReturn<'getAllShopProductsForId', DataT, PickKeys, DefaultT>;
@@ -1,8 +1,12 @@
1
1
  import { useRpc } from "@scayle/storefront-nuxt/composables";
2
- export function useAllShopProductsById(params, key = "useAllShopProductsById") {
2
+ export function useAllShopProductsForId({
3
+ params,
4
+ options
5
+ } = {}, key = "useAllShopProductsById") {
3
6
  return useRpc(
4
7
  "getAllShopProductsForId",
5
8
  key,
6
- params
9
+ params,
10
+ options
7
11
  );
8
12
  }
@@ -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,89 @@
1
+ import { useCurrentShop, useRpcCall } from "@scayle/storefront-nuxt/composables";
2
+ import { useLocalStorage } from "@vueuse/core";
3
+ import { useState } from "nuxt/app";
4
+ export function useRecentlyViewedProducts(options) {
5
+ const shop = useCurrentShop();
6
+ const recentlyViewedProductsIds = useLocalStorage(
7
+ `${shop.value.shopId}-recently-viewed-products`,
8
+ []
9
+ );
10
+ const fetchProductsRpc = useRpcCall("getProductsByIds");
11
+ const loading = useState("recently-viewed-products-loading", () => false);
12
+ const error = useState("recently-viewed-products-error");
13
+ const status = useState(
14
+ "recently-viewed-products-status",
15
+ () => "idle"
16
+ );
17
+ const products = useState("recently-viewed-products", () => []);
18
+ const addProductId = (productId) => {
19
+ if (import.meta.server) {
20
+ throw new Error(
21
+ "useRecentlyViewedProducts uses local storage. addProductId is therefore not available on the server."
22
+ );
23
+ }
24
+ const productIds = [...recentlyViewedProductsIds.value];
25
+ if (productIds.includes(productId)) {
26
+ productIds.splice(productIds.indexOf(productId), 1);
27
+ }
28
+ productIds.unshift(productId);
29
+ if (productIds.length > 10) {
30
+ productIds.pop();
31
+ }
32
+ recentlyViewedProductsIds.value = productIds;
33
+ };
34
+ const loadMissingProducts = async () => {
35
+ if (import.meta.server) {
36
+ throw new Error(
37
+ "useRecentlyViewedProducts uses local storage. fetchProducts is therefore not available on the server."
38
+ );
39
+ }
40
+ loading.value = true;
41
+ status.value = "pending";
42
+ try {
43
+ const loadedProductIds = new Set(
44
+ products.value.map((product) => product.id)
45
+ );
46
+ const missingProductIds = recentlyViewedProductsIds.value.filter(
47
+ (id) => !loadedProductIds.has(id)
48
+ );
49
+ if (missingProductIds.length > 0) {
50
+ const result = await fetchProductsRpc({
51
+ ids: missingProductIds,
52
+ with: options?.with,
53
+ pricePromotionKey: options?.pricePromotionKey
54
+ });
55
+ products.value.push(...result);
56
+ }
57
+ const sortOrderMap = new Map(
58
+ recentlyViewedProductsIds.value.map(
59
+ (id, index) => [id, index]
60
+ )
61
+ );
62
+ products.value = products.value.sort((a, b) => {
63
+ const aIndex = sortOrderMap.get(a.id);
64
+ const bIndex = sortOrderMap.get(b.id);
65
+ if (aIndex === void 0) {
66
+ return 1;
67
+ }
68
+ if (bIndex === void 0) {
69
+ return -1;
70
+ }
71
+ return aIndex - bIndex;
72
+ }).slice(0, 10);
73
+ status.value = "success";
74
+ } catch (e) {
75
+ error.value = e;
76
+ status.value = "error";
77
+ } finally {
78
+ loading.value = false;
79
+ }
80
+ };
81
+ return {
82
+ addProductId,
83
+ loadMissingProducts,
84
+ products,
85
+ loading,
86
+ error,
87
+ status
88
+ };
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-product-detail",
3
- "version": "1.4.1",
3
+ "version": "1.5.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",
@@ -27,23 +27,6 @@
27
27
  "imports": {
28
28
  "#storefront-product-detail": "./dist/runtime/index.js"
29
29
  },
30
- "scripts": {
31
- "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",
35
- "lint": "eslint .",
36
- "lint:ci": "eslint . --format gitlab",
37
- "lint:fix": "eslint . --fix",
38
- "format": "dprint check",
39
- "format:fix": "dprint fmt",
40
- "test": "vitest --run",
41
- "test:watch": "vitest",
42
- "test:ci": "vitest --run --coverage --reporter=default --reporter=junit --outputFile=./coverage/junit.xml",
43
- "typecheck": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
44
- "package:lint": "publint",
45
- "verify-packaging": "attw --pack . --profile esm-only"
46
- },
47
30
  "peerDependencies": {
48
31
  "@nuxt/kit": ">=3.13.0",
49
32
  "@scayle/storefront-nuxt": "^8.0.0",
@@ -54,28 +37,46 @@
54
37
  "vue": "^3.5.13"
55
38
  },
56
39
  "devDependencies": {
57
- "@arethetypeswrong/cli": "0.18.1",
40
+ "@arethetypeswrong/cli": "0.18.2",
58
41
  "@nuxt/kit": "3.16.2",
59
42
  "@nuxt/module-builder": "1.0.1",
60
43
  "@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",
44
+ "@nuxt/test-utils": "3.19.2",
45
+ "@scayle/eslint-config-storefront": "4.5.12",
46
+ "@scayle/storefront-nuxt": "8.32.0",
47
+ "@types/node": "22.15.34",
48
+ "@vitest/coverage-v8": "3.2.4",
65
49
  "@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",
50
+ "@vueuse/core": "13.4.0",
51
+ "dprint": "0.50.1",
52
+ "eslint-formatter-gitlab": "6.0.1",
53
+ "eslint": "9.30.0",
54
+ "happy-dom": "18.0.1",
71
55
  "nuxt": "3.16.2",
72
- "publint": "0.2.12",
56
+ "publint": "0.3.12",
73
57
  "schema-dts": "1.1.5",
74
58
  "typescript": "5.8.3",
75
59
  "unbuild": "3.5.0",
76
- "vitest": "3.1.4",
60
+ "vitest": "3.2.4",
77
61
  "vue-router": "4.5.1",
78
62
  "vue-tsc": "2.2.10",
79
- "vue": "3.5.14"
63
+ "vue": "3.5.17"
64
+ },
65
+ "scripts": {
66
+ "build": "nuxt-module-build build",
67
+ "dev": "nuxt dev playground",
68
+ "dev:build": "nuxt build playground",
69
+ "prep": "nuxt-module-build prepare && nuxt prepare playground",
70
+ "lint": "eslint .",
71
+ "lint:ci": "eslint . --format gitlab",
72
+ "lint:fix": "eslint . --fix",
73
+ "format": "dprint check",
74
+ "format:fix": "dprint fmt",
75
+ "test": "vitest --run",
76
+ "test:watch": "vitest",
77
+ "test:ci": "vitest --run --coverage --reporter=default --reporter=junit --outputFile=./coverage/junit.xml",
78
+ "typecheck": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
79
+ "package:lint": "publint",
80
+ "verify-packaging": "attw --pack . --profile esm-only"
80
81
  }
81
- }
82
+ }