@webround/commerce-react 0.3.14 → 0.4.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/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { useResource } from './use-resource.js';
2
- export type { UseResourceOptions, UseResourceResult } from './use-resource.js';
2
+ export type { UseResourceOptions, UseResourceResult, UseResourceState } from './use-resource.js';
3
+ export { storeContentOf, useStoreData } from './use-store-data.js';
4
+ export type { StoreContent, UseStoreDataResult } from './use-store-data.js';
3
5
  export { useShop } from './use-shop.js';
4
6
  export type { UseShopOptions, UseShopResult } from './use-shop.js';
5
7
  export { useShopAddress } from './use-shop-address.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // @webround/commerce-react: what a React component needs to use a store (React 19). The logic is `@webround/commerce`'s; this only binds it to components.
2
2
  export { useResource } from './use-resource.js';
3
+ export { storeContentOf, useStoreData } from './use-store-data.js';
3
4
  export { useShop } from './use-shop.js';
4
5
  export { useShopAddress } from './use-shop-address.js';
5
6
  export { authActions, useAuth } from './use-auth.js';
@@ -6,11 +6,27 @@ export interface UseResourceOptions {
6
6
  */
7
7
  keepPrevious?: boolean;
8
8
  }
9
- export interface UseResourceResult<T> {
9
+ /**
10
+ * What `data` is. The backends wrap a list or a record as `{ data, pagination… }`; a component wants the list, not the wrapper, so an answer that has a `data` key is opened: its `data` is `data`, and each of
11
+ * its other keys (`pagination`, `breakdown`) is a key of the result, next to it. An answer with no `data` key (an order, a return) is `data` as it is. One rule, for every hook.
12
+ */
13
+ export type Payload<T> = T extends {
14
+ data: infer D;
15
+ } ? D : T;
16
+ /** The keys of the wrapper that are not `data`: there from the moment the answer is, `undefined` before. */
17
+ export type Beside<T> = T extends {
18
+ data: unknown;
19
+ } ? Partial<Omit<T, 'data'>> : unknown;
20
+ /** The answer in the shape a component reads it (see `Payload`). The wrapper's other keys never meet the result's own (`status`, `error`…): the result's are written last, and a test keeps the backends from taking one. */
21
+ export declare function presented<T>(answer: T | undefined): {
22
+ data: Payload<T> | undefined;
23
+ } & Beside<T>;
24
+ export type UseResourceResult<T> = UseResourceState<Payload<T>> & Beside<T>;
25
+ export interface UseResourceState<D> {
10
26
  /** `idle`: nothing is asked (no resource was given). `pending`: the answer is being waited for. `ready`: it is there. `error`: it could not be had. */
11
27
  status: 'idle' | 'pending' | 'ready' | 'error';
12
- /** The answer to what is asked, or (with `keepPrevious`, while the new one is awaited) the answer to the question before; undefined when there is none. */
13
- data: T | undefined;
28
+ /** The answer to what is asked (the list, the record: never the backend's wrapper), or (with `keepPrevious`, while the new one is awaited) the answer to the question before; undefined when there is none. */
29
+ data: D | undefined;
14
30
  /** Why it failed (a `CommerceError` for what the store did, or a network error): set with `status: 'error'`, undefined otherwise. */
15
31
  error: unknown;
16
32
  /** The answer to what is asked is being waited for. */
@@ -1,4 +1,9 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
2
+ /** The answer in the shape a component reads it (see `Payload`). The wrapper's other keys never meet the result's own (`status`, `error`…): the result's are written last, and a test keeps the backends from taking one. */
3
+ export function presented(answer) {
4
+ const wrapped = typeof answer === 'object' && answer !== null && 'data' in answer;
5
+ return (wrapped ? { ...answer } : { data: answer });
6
+ }
2
7
  const NOTHING = { status: 'idle' };
3
8
  const noSubscription = () => () => undefined;
4
9
  /** A resource asked for by a component: `null`/`undefined` is "not asked yet" (a slug that is not known, a search that is not made). */
@@ -47,14 +52,15 @@ export function useResource(resource, options = {}) {
47
52
  hasShown.current = true;
48
53
  }
49
54
  }, [snapshot]);
55
+ const result = (state, answer) => ({ ...presented(answer), ...state });
50
56
  if (!asked)
51
- return { status: 'idle', data: undefined, error: undefined, isFetching: false, isStale: false, retry };
57
+ return result({ status: 'idle', error: undefined, isFetching: false, isStale: false, retry }, undefined);
52
58
  if (snapshot.status === 'ready')
53
- return { status: 'ready', data: snapshot.data, error: undefined, isFetching: false, isStale: false, retry };
59
+ return result({ status: 'ready', error: undefined, isFetching: false, isStale: false, retry }, snapshot.data);
54
60
  if (snapshot.status === 'error')
55
- return { status: 'error', data: undefined, error: snapshot.error, isFetching: false, isStale: false, retry };
61
+ return result({ status: 'error', error: snapshot.error, isFetching: false, isStale: false, retry }, undefined);
56
62
  // `idle` here is a question that is about to be asked (the effect has not run yet): it is awaited, there is no frame in which it is "nothing"
57
63
  const stale = keepPrevious && hasShown.current;
58
- return { status: 'pending', data: stale ? shown.current : undefined, error: undefined, isFetching: true, isStale: stale, retry };
64
+ return result({ status: 'pending', error: undefined, isFetching: true, isStale: stale, retry }, stale ? shown.current : undefined);
59
65
  }
60
66
  //# sourceMappingURL=use-resource.js.map
@@ -1,10 +1,11 @@
1
1
  import type { CachedCatalog } from '@webround/commerce';
2
- import type { ProductPage, ProductSearch, StoreDataResponse } from '@webround/commerce';
2
+ import type { Pagination, ProductSearch, ProductSummary } from '@webround/commerce';
3
3
  import { type SortOption } from '@webround/commerce';
4
4
  import type { ShopState } from '@webround/commerce';
5
5
  import { type ShopActions, type ShopStore } from '@webround/commerce';
6
6
  import type { SearchContext } from '@webround/commerce';
7
- import { type UseResourceResult } from './use-resource.js';
7
+ import { type UseResourceState } from './use-resource.js';
8
+ import { type StoreContent } from './use-store-data.js';
8
9
  export interface UseShopOptions {
9
10
  catalog: CachedCatalog;
10
11
  /** What the question needs besides what the customer chose: the currency, the language, the cadence, what the products are loaded with. */
@@ -16,19 +17,20 @@ export interface UseShopOptions {
16
17
  /** While a new question is awaited, keep showing the answer to the one before (marked `isStale`), so a listing does not empty at every click. On by default. */
17
18
  keepPrevious?: boolean;
18
19
  }
19
- export interface UseShopResult {
20
- /** What the customer chose. */
20
+ /** The products and how they are being asked: `status` and the rest are about THE PRODUCTS (they say `pending` or `error` while the store's data, which the question needs, is awaited or failed). */
21
+ export interface UseShopResult extends StoreContent, Omit<UseResourceState<ProductSummary[]>, 'data'> {
22
+ /** What the customer chose: the collection, the text, the tag values and ranges, the order, the page. */
21
23
  state: ShopState;
22
- /** What changes it. */
24
+ /** What changes it. Every change that is not `setPage` goes back to page 1. */
23
25
  actions: ShopActions;
24
- /** The store's own data (its tags, collections, filters, currencies): what the controls are drawn from. */
25
- storeData: UseResourceResult<StoreDataResponse>;
26
26
  /** The orders that can be offered now (with the store's data in hand, and a text or not); none until it is. */
27
27
  sortOptions: SortOption[];
28
- /** The question the catalog is being asked now, or null while the store's data is awaited. */
28
+ /** The exact question the catalog is being asked now (`state` made a request, with `context`), or `null` while the store's data is awaited. The `products` are its answer. */
29
29
  question: ProductSearch | null;
30
- /** The products. While the store's data is awaited this is `pending` (or its error, with a `retry` that asks for the store again). */
31
- results: UseResourceResult<ProductPage>;
30
+ /** The products of the page the customer is at, for `question`. `undefined` until they are there; while the next page is awaited, with `keepPrevious`, the page before (`isStale`). */
31
+ products: ProductSummary[] | undefined;
32
+ /** How many products and pages there are for `question`, and which page this is. `undefined` when `products` is. */
33
+ pagination: Pagination | undefined;
32
34
  }
33
35
  export declare function useShop(options: UseShopOptions): UseShopResult;
34
36
  //# sourceMappingURL=use-shop.d.ts.map
package/dist/use-shop.js CHANGED
@@ -4,6 +4,7 @@ import { shopQuestion } from '@webround/commerce';
4
4
  import { sortOptionsOf } from '@webround/commerce';
5
5
  import { createShopStore, shopActions } from '@webround/commerce';
6
6
  import { useResource } from './use-resource.js';
7
+ import { useStoreData } from './use-store-data.js';
7
8
  export function useShop(options) {
8
9
  const { catalog, context, keepPrevious = true } = options;
9
10
  // The state: in memory, in the hook, unless a store was given
@@ -13,19 +14,21 @@ export function useShop(options) {
13
14
  const store = options.store ?? own.current;
14
15
  const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
15
16
  const actions = useMemo(() => shopActions(store), [store]);
16
- const storeData = useResource(catalog.resource.storeData.get(context.langCode === undefined ? undefined : { langCode: context.langCode }));
17
- const data = storeData.data?.data;
17
+ const storeData = useStoreData(catalog, context.langCode);
18
+ const { status: storeStatus, error: storeError, retry: retryStore, ...content } = storeData;
19
+ const loaded = content.store !== undefined;
18
20
  // A new `context` object at every render is the same context: only what is in it makes a new question
19
21
  const contextKey = canonicalJson(context);
20
22
  // eslint-disable-next-line react-hooks/exhaustive-deps
21
- const asked = useMemo(() => shopQuestion(catalog, data, state, context), [catalog, data, state, contextKey]);
23
+ const asked = useMemo(() => shopQuestion(catalog, loaded ? content : null, state, context), [catalog, storeData, state, contextKey]);
22
24
  const products = useResource(asked?.resource ?? null, { keepPrevious });
23
- const sortOptions = useMemo(() => (data ? sortOptionsOf(data) : []), [data]);
25
+ const sortOptions = useMemo(() => (loaded ? sortOptionsOf(content) : []), [storeData]);
24
26
  // With no question yet the products are waiting for the store's data (or failed with it): say that, so the screen has one thing to look at
25
- const retry = useCallback(() => storeData.retry(), [storeData]);
26
- const results = asked
27
+ const retry = useCallback(() => retryStore(), [retryStore]);
28
+ const listing = asked
27
29
  ? products
28
- : { status: storeData.status === 'error' ? 'error' : 'pending', data: undefined, error: storeData.error, isFetching: storeData.status !== 'error', isStale: false, retry };
29
- return { state, actions, storeData, sortOptions, question: asked?.question ?? null, results };
30
+ : { status: storeStatus === 'error' ? 'error' : 'pending', data: undefined, error: storeError, isFetching: storeStatus !== 'error', isStale: false, retry };
31
+ const { data, pagination, ...how } = listing;
32
+ return { ...content, state, actions, sortOptions, question: asked?.question ?? null, products: data, pagination, ...how };
30
33
  }
31
34
  //# sourceMappingURL=use-shop.js.map
@@ -0,0 +1,25 @@
1
+ import type { CachedCatalog, StoreAttribute, StoreCollection, StoreData, StoreFilter, StoreSettings, StoreTag } from '@webround/commerce';
2
+ import { type UseResourceResult } from './use-resource.js';
3
+ /** What the store's data says, by name. The lists are empty until the data is there; `store` is `undefined` until then (it is how a component knows the data has come). */
4
+ export interface StoreContent {
5
+ /** The store's settings: its currency, cadence, the languages and currencies it offers. `undefined` until the data is there. */
6
+ store: StoreSettings | undefined;
7
+ /** Every collection, as a tree (the children are in `children`); the invisible ones too (`visible`). */
8
+ collections: StoreCollection[];
9
+ /** The tags, each with its values. */
10
+ tags: StoreTag[];
11
+ attributes: StoreAttribute[];
12
+ /** The filters a customer can set (ranges: a price, a number, a date). */
13
+ filters: StoreFilter[];
14
+ }
15
+ export interface UseStoreDataResult extends StoreContent {
16
+ /** `pending` until the store's data is there, `error` if it could not be had (`retry` asks again). */
17
+ status: UseResourceResult<unknown>['status'];
18
+ error: unknown;
19
+ retry: () => void;
20
+ }
21
+ /** The store's data by name. Pure: the same data gives the same lists (the same arrays). */
22
+ export declare function storeContentOf(data: StoreData | undefined): StoreContent;
23
+ /** `langCode` is the language the names are asked in (left out: the store's own). */
24
+ export declare function useStoreData(catalog: Pick<CachedCatalog, 'resource'>, langCode?: string): UseStoreDataResult;
25
+ //# sourceMappingURL=use-store-data.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { useMemo } from 'react';
2
+ import { useResource } from './use-resource.js';
3
+ // ONE empty content: the lists before the data is there are always the same arrays, so what depends on them is not drawn again for nothing
4
+ const NOT_THERE = { store: undefined, collections: [], tags: [], attributes: [], filters: [] };
5
+ /** The store's data by name. Pure: the same data gives the same lists (the same arrays). */
6
+ export function storeContentOf(data) {
7
+ if (!data)
8
+ return NOT_THERE;
9
+ return { store: data.store, collections: data.collections, tags: data.tags, attributes: data.attributes, filters: data.filters };
10
+ }
11
+ /** `langCode` is the language the names are asked in (left out: the store's own). */
12
+ export function useStoreData(catalog, langCode) {
13
+ const answer = useResource(catalog.resource.storeData.get(langCode === undefined ? undefined : { langCode }));
14
+ const content = useMemo(() => storeContentOf(answer.data), [answer.data]);
15
+ return useMemo(() => ({ ...content, status: answer.status, error: answer.error, retry: answer.retry }), [content, answer.status, answer.error, answer.retry]);
16
+ }
17
+ //# sourceMappingURL=use-store-data.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webround/commerce-react",
3
- "version": "0.3.14",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -28,8 +28,8 @@
28
28
  "typecheck": "tsc --noEmit -p tsconfig.test.json"
29
29
  },
30
30
  "peerDependencies": {
31
- "@webround/commerce": "^0.3.0",
32
- "@webround/core": "^0.3.0",
31
+ "@webround/commerce": "^0.4.0",
32
+ "@webround/core": "^0.4.0",
33
33
  "react": ">=19"
34
34
  },
35
35
  "devDependencies": {