@stapel/listings-react 0.18.2 → 0.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.
- package/CHANGELOG.md +41 -0
- package/dist/api/generated/schema.d.ts +101 -13
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/listingsApi.d.ts +26 -1
- package/dist/api/listingsApi.d.ts.map +1 -1
- package/dist/api/listingsApi.js +9 -0
- package/dist/api/listingsApi.js.map +1 -1
- package/dist/api/types.d.ts +91 -6
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js +32 -0
- package/dist/api/types.js.map +1 -1
- package/dist/default/GateReasonPopover.d.ts.map +1 -1
- package/dist/default/GateReasonPopover.js +47 -2
- package/dist/default/GateReasonPopover.js.map +1 -1
- package/dist/default/ListingCard.d.ts +32 -1
- package/dist/default/ListingCard.d.ts.map +1 -1
- package/dist/default/ListingCard.js +65 -14
- package/dist/default/ListingCard.js.map +1 -1
- package/dist/default/ListingDetailPane.d.ts.map +1 -1
- package/dist/default/ListingDetailPane.js +12 -7
- package/dist/default/ListingDetailPane.js.map +1 -1
- package/dist/default/ListingFeedCard.d.ts +32 -7
- package/dist/default/ListingFeedCard.d.ts.map +1 -1
- package/dist/default/ListingFeedCard.js +11 -3
- package/dist/default/ListingFeedCard.js.map +1 -1
- package/dist/default/ListingPrice.d.ts +13 -2
- package/dist/default/ListingPrice.d.ts.map +1 -1
- package/dist/default/ListingPrice.js.map +1 -1
- package/dist/default/ListingSerpCard.d.ts.map +1 -1
- package/dist/default/ListingSerpCard.js +10 -2
- package/dist/default/ListingSerpCard.js.map +1 -1
- package/dist/default/favorite.d.ts +11 -0
- package/dist/default/favorite.d.ts.map +1 -1
- package/dist/default/favorite.js +24 -9
- package/dist/default/favorite.js.map +1 -1
- package/dist/default/icons.d.ts +17 -3
- package/dist/default/icons.d.ts.map +1 -1
- package/dist/default/icons.js +2 -1
- package/dist/default/icons.js.map +1 -1
- package/dist/default/index.d.ts +1 -1
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +4 -1
- package/dist/default/index.js.map +1 -1
- package/dist/headless/Engagement.d.ts +62 -0
- package/dist/headless/Engagement.d.ts.map +1 -0
- package/dist/headless/Engagement.js +116 -0
- package/dist/headless/Engagement.js.map +1 -0
- package/dist/headless/Favorites.d.ts +50 -0
- package/dist/headless/Favorites.d.ts.map +1 -1
- package/dist/headless/Favorites.js +63 -3
- package/dist/headless/Favorites.js.map +1 -1
- package/dist/headless/ListingDetail.d.ts +21 -1
- package/dist/headless/ListingDetail.d.ts.map +1 -1
- package/dist/headless/ListingDetail.js +39 -3
- package/dist/headless/ListingDetail.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +4 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/generated/errors.es.gen.d.ts +1 -1
- package/dist/i18n/generated/errors.es.gen.js +1 -1
- package/dist/i18n/generated/errors.gen.d.ts +12 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +6 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.d.ts +1 -1
- package/dist/i18n/generated/errors.ru.gen.js +1 -1
- package/dist/i18n/keys.d.ts +7 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +8 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +6 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/model/engagement.d.ts +85 -0
- package/dist/model/engagement.d.ts.map +1 -0
- package/dist/model/engagement.js +59 -0
- package/dist/model/engagement.js.map +1 -0
- package/dist/model/queries.d.ts +28 -1
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +36 -0
- package/dist/model/queries.js.map +1 -1
- package/dist/model/queryKeys.d.ts +12 -13
- package/dist/model/queryKeys.d.ts.map +1 -1
- package/dist/model/queryKeys.js +15 -0
- package/dist/model/queryKeys.js.map +1 -1
- package/llms.txt +6 -3
- package/manifest.json +71 -3
- package/nav-manifest.json +1 -1
- package/package.json +8 -8
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +101 -13
- package/src/api/listingsApi.ts +39 -0
- package/src/api/types.ts +115 -6
- package/src/default/GateReasonPopover.tsx +62 -3
- package/src/default/ListingCard.tsx +84 -10
- package/src/default/ListingDetailPane.tsx +48 -16
- package/src/default/ListingFeedCard.tsx +45 -8
- package/src/default/ListingPrice.tsx +13 -2
- package/src/default/ListingSerpCard.tsx +11 -1
- package/src/default/favorite.tsx +49 -21
- package/src/default/icons.tsx +21 -3
- package/src/default/index.ts +4 -0
- package/src/headless/Engagement.tsx +177 -0
- package/src/headless/Favorites.tsx +93 -3
- package/src/headless/ListingDetail.tsx +64 -4
- package/src/i18n/es.ts +7 -0
- package/src/i18n/generated/errors.es.gen.ts +1 -1
- package/src/i18n/generated/errors.gen.ts +6 -0
- package/src/i18n/generated/errors.json +14 -0
- package/src/i18n/generated/errors.ru.gen.ts +1 -1
- package/src/i18n/keys.ts +8 -0
- package/src/i18n/ru.ts +9 -0
- package/src/index.ts +27 -0
- package/src/model/engagement.ts +118 -0
- package/src/model/queries.ts +41 -0
- package/src/model/queryKeys.ts +14 -0
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Namespaced TanStack Query keys (frontend-standard §2 — namespaced keys).
|
|
3
|
-
* Everything under the `"listings"` root so a host can invalidate the whole
|
|
4
|
-
* module or match a single read. Explicit tuple return types satisfy
|
|
5
|
-
* `--isolatedDeclarations`.
|
|
6
|
-
*
|
|
7
|
-
* THE KEY IS THE REQUEST for the paginated reads: a page is keyed on the very
|
|
8
|
-
* parameter object the client is handed, normalized so that "no cursor" and
|
|
9
|
-
* "an explicitly undefined cursor" cannot cache twice. Two states that
|
|
10
|
-
* produce the same request therefore share a cache entry, and one that
|
|
11
|
-
* produces a different request cannot silently reuse a page — the stale-page
|
|
12
|
-
* bug (new filter, cached rows) is unwritable rather than merely avoided.
|
|
13
|
-
*/
|
|
14
1
|
import type { ListingPageParams } from "../api/types.js";
|
|
15
2
|
/** The normalized page cursor a list read is keyed on. */
|
|
16
3
|
export interface ListingPageKey {
|
|
@@ -51,5 +38,17 @@ export declare const listingsQueryKeys: {
|
|
|
51
38
|
allFavorites(): readonly ["listings", "my", "favorites"];
|
|
52
39
|
/** Every published-card PAGE, same reason. */
|
|
53
40
|
allLists(): readonly ["listings", "list"];
|
|
41
|
+
/**
|
|
42
|
+
* The per-viewer overlay for one page of ids.
|
|
43
|
+
*
|
|
44
|
+
* Keyed on the NORMALIZED list (`engagementIds`: sorted, de-duplicated,
|
|
45
|
+
* capped) and joined into one string, which is the same value the request
|
|
46
|
+
* sends. The doctrine at the top of this file applied to a batch read: two
|
|
47
|
+
* renders asking for the same ids in a different order are asking the
|
|
48
|
+
* identical question — the answer is a map keyed by id and carries no order
|
|
49
|
+
* of its own — so they must share one cache entry and cost one request.
|
|
50
|
+
* Keyed on the raw array they would cost two, per re-render, per grid.
|
|
51
|
+
*/
|
|
52
|
+
engagement(ids: readonly number[]): readonly ["listings", "engagement", string];
|
|
54
53
|
};
|
|
55
54
|
//# sourceMappingURL=queryKeys.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryKeys.d.ts","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"queryKeys.d.ts","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;wBAEwB;AACxB,wBAAgB,OAAO,CAAC,MAAM,CAAC,EAAE,iBAAiB,GAAG,cAAc,CAMlE;AAID,eAAO,MAAM,iBAAiB,EAAE;IAC9B,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,+BAA+B;IAC/B,IAAI,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1E,2BAA2B;IAC3B,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5D;;0EAEsE;IACtE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5D,oCAAoC;IACpC,UAAU,IAAI,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;IACtD,gDAAgD;IAChD,WAAW,CACT,IAAI,EAAE,cAAc,GACnB,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;IAC5D,mEAAmE;IACnE,IAAI,CACF,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,cAAc,GACnB,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC;IACnE;;sCAEkC;IAClC,YAAY,IAAI,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;IACpE,+DAA+D;IAC/D,OAAO,IAAI,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC;IACnD,8DAA8D;IAC9D,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,gBAAgB,EAAE,MAAM,CAAC,CAAC;IAC3E;8EAC0E;IAC1E,YAAY,IAAI,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IACzD,8CAA8C;IAC9C,QAAQ,IAAI,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC1C;;;;;;;;;;OAUG;IACH,UAAU,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC,UAAU,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC;CAejF,CAAC"}
|
package/dist/model/queryKeys.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespaced TanStack Query keys (frontend-standard §2 — namespaced keys).
|
|
3
|
+
* Everything under the `"listings"` root so a host can invalidate the whole
|
|
4
|
+
* module or match a single read. Explicit tuple return types satisfy
|
|
5
|
+
* `--isolatedDeclarations`.
|
|
6
|
+
*
|
|
7
|
+
* THE KEY IS THE REQUEST for the paginated reads: a page is keyed on the very
|
|
8
|
+
* parameter object the client is handed, normalized so that "no cursor" and
|
|
9
|
+
* "an explicitly undefined cursor" cannot cache twice. Two states that
|
|
10
|
+
* produce the same request therefore share a cache entry, and one that
|
|
11
|
+
* produces a different request cannot silently reuse a page — the stale-page
|
|
12
|
+
* bug (new filter, cached rows) is unwritable rather than merely avoided.
|
|
13
|
+
*/
|
|
14
|
+
import { engagementIds } from "../api/types.js";
|
|
1
15
|
/** Normalize a page's parameters into its key. Exported because a test that
|
|
2
16
|
* asserts "these two states hit one cache entry" must build the key the same
|
|
3
17
|
* way the hook does. */
|
|
@@ -22,5 +36,6 @@ export const listingsQueryKeys = {
|
|
|
22
36
|
validateDraft: (id) => [ROOT, "validate-draft", id],
|
|
23
37
|
allFavorites: () => [ROOT, "my", "favorites"],
|
|
24
38
|
allLists: () => [ROOT, "list"],
|
|
39
|
+
engagement: (ids) => [ROOT, "engagement", engagementIds(ids).join(",")],
|
|
25
40
|
};
|
|
26
41
|
//# sourceMappingURL=queryKeys.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queryKeys.js","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"queryKeys.js","sourceRoot":"","sources":["../../src/model/queryKeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAUhD;;wBAEwB;AACxB,MAAM,UAAU,OAAO,CAAC,MAA0B;IAChD,OAAO;QACL,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,IAAI;QAC9B,SAAS,EAAE,MAAM,EAAE,SAAS,IAAI,IAAI;QACpC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,IAAI;KAC7B,CAAC;AACJ,CAAC;AAED,MAAM,IAAI,GAAG,UAAmB,CAAC;AAEjC,MAAM,CAAC,MAAM,iBAAiB,GA8C1B;IACF,GAAG,EAAE,CAAC,IAAI,CAAC;IACX,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC;IACpC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC;IACpC,MAAM,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC;IACpC,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC;IACtD,IAAI,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,CAAC;IACxD,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC;IACxD,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC;IACvC,aAAa,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,gBAAgB,EAAE,EAAE,CAAC;IACnD,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,CAAC;IAC7C,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC;IAC9B,UAAU,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,YAAY,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;CACxE,CAAC"}
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @stapel/listings-react 0.
|
|
1
|
+
# @stapel/listings-react 0.19.0
|
|
2
2
|
|
|
3
|
-
Headless React flow pair for stapel-listings (contract >=0.
|
|
3
|
+
Headless React flow pair for stapel-listings (contract >=0.17 <0.18) — business + state, zero visual opinion.
|
|
4
4
|
Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
|
|
5
5
|
verification-403 interception, i18n engine, analytics facade, TanStack Query layer.
|
|
6
6
|
|
|
@@ -25,6 +25,7 @@ Paths are relative to `/listings/api/v1/`.
|
|
|
25
25
|
- listings_api_v1_listings_complete_create: POST /listings/{id}/complete/
|
|
26
26
|
- listings_api_v1_listings_create: POST /listings/
|
|
27
27
|
- listings_api_v1_listings_destroy: DELETE /listings/{id}/
|
|
28
|
+
- listings_api_v1_listings_engagement_retrieve: GET /listings/engagement/
|
|
28
29
|
- listings_api_v1_listings_favorite_create: POST /listings/{id}/favorite/
|
|
29
30
|
- listings_api_v1_listings_list: GET /listings/
|
|
30
31
|
- listings_api_v1_listings_my_counters_retrieve: GET /listings/my/counters/
|
|
@@ -47,6 +48,7 @@ Paths are relative to `/listings/api/v1/`.
|
|
|
47
48
|
- useFavoriteListing (mutation) → favorite+unfavorite
|
|
48
49
|
- useListing (query) → retrieve
|
|
49
50
|
- useListingCards (query) → list
|
|
51
|
+
- useListingEngagement (query) → engagement
|
|
50
52
|
- useListingStatus (query) → status
|
|
51
53
|
- useMyCounters (query) → myCounters
|
|
52
54
|
- useMyFavorites (query) → myFavorites
|
|
@@ -55,7 +57,7 @@ Paths are relative to `/listings/api/v1/`.
|
|
|
55
57
|
- useValidateDraft (query) → validateDraft
|
|
56
58
|
|
|
57
59
|
## Errors (render t(code, params); UX from remediation)
|
|
58
|
-
|
|
60
|
+
68 keys (full catalog: manifest.json §errors). By remediation: fix_input 39 · retry 16 · verify 5 · wait_and_retry 5 · contact_support 2 · reauthenticate 1.
|
|
59
61
|
Param-bearing keys (interpolation slots matter):
|
|
60
62
|
- error.400.description_too_long [400] → fix_input {max_length}
|
|
61
63
|
- error.400.description_too_short [400] → fix_input {min_length}
|
|
@@ -109,6 +111,7 @@ const { tracked } = useTracked();
|
|
|
109
111
|
- listings.card → <ListingCard> [default|saved|visitor|grid cell|list row|no-photo] demo/ListingCard.demo.tsx
|
|
110
112
|
- listings.composer → <ListingComposerPage> [default|unwired|photos-in-flight|unsupported-type] demo/ListingComposer.demo.tsx
|
|
111
113
|
- listings.detail → <ListingDetailPane> [default|contact-unwired|owner|not-found] demo/ListingDetail.demo.tsx
|
|
114
|
+
- listings.engagement-scope → <ListingEngagementScope> [default|unscoped|visitor|failed] demo/ListingEngagementScope.demo.tsx
|
|
112
115
|
- listings.favorites → <FavoritesPane> [default|paged|empty|failed|visitor] demo/Favorites.demo.tsx
|
|
113
116
|
- listings.feed-card → <ListingFeedCard> [default|tile|visitor|wide] demo/ListingFeedCard.demo.tsx
|
|
114
117
|
- listings.mine → <MyListingsPane> [default|no-editor|taken-down|visitor|failed] demo/MyListings.demo.tsx
|
package/manifest.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
|
|
3
3
|
"package": "@stapel/listings-react",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.19.0",
|
|
5
5
|
"backend": {
|
|
6
6
|
"module": "stapel-listings",
|
|
7
|
-
"contract": ">=0.
|
|
7
|
+
"contract": ">=0.17 <0.18"
|
|
8
8
|
},
|
|
9
9
|
"layers": [
|
|
10
10
|
"api",
|
|
@@ -44,6 +44,13 @@
|
|
|
44
44
|
"request": null,
|
|
45
45
|
"response": "DeleteResponse"
|
|
46
46
|
},
|
|
47
|
+
"listings_api_v1_listings_engagement_retrieve": {
|
|
48
|
+
"method": "GET",
|
|
49
|
+
"path": "/listings/api/v1/listings/engagement/",
|
|
50
|
+
"tag": "listings",
|
|
51
|
+
"request": null,
|
|
52
|
+
"response": "ListingEngagementBatch"
|
|
53
|
+
},
|
|
47
54
|
"listings_api_v1_listings_favorite_create": {
|
|
48
55
|
"method": "POST",
|
|
49
56
|
"path": "/listings/api/v1/listings/{id}/favorite/",
|
|
@@ -211,6 +218,15 @@
|
|
|
211
218
|
"{page}"
|
|
212
219
|
]
|
|
213
220
|
},
|
|
221
|
+
"useListingEngagement": {
|
|
222
|
+
"kind": "query",
|
|
223
|
+
"operation": "engagement",
|
|
224
|
+
"queryKey": [
|
|
225
|
+
"listings",
|
|
226
|
+
"engagement",
|
|
227
|
+
"{…}"
|
|
228
|
+
]
|
|
229
|
+
},
|
|
214
230
|
"useListingStatus": {
|
|
215
231
|
"kind": "query",
|
|
216
232
|
"operation": "status",
|
|
@@ -479,6 +495,16 @@
|
|
|
479
495
|
],
|
|
480
496
|
"remediation": "fix_input"
|
|
481
497
|
},
|
|
498
|
+
"error.400.listing_location_required": {
|
|
499
|
+
"status": 400,
|
|
500
|
+
"params": [],
|
|
501
|
+
"remediation": "fix_input"
|
|
502
|
+
},
|
|
503
|
+
"error.400.listing_zero_price_not_allowed": {
|
|
504
|
+
"status": 400,
|
|
505
|
+
"params": [],
|
|
506
|
+
"remediation": "fix_input"
|
|
507
|
+
},
|
|
482
508
|
"error.400.publish_validation_failed": {
|
|
483
509
|
"status": 400,
|
|
484
510
|
"params": [],
|
|
@@ -713,6 +739,26 @@
|
|
|
713
739
|
],
|
|
714
740
|
"source": "demo/ListingDetail.demo.tsx"
|
|
715
741
|
},
|
|
742
|
+
{
|
|
743
|
+
"id": "listings.engagement-scope",
|
|
744
|
+
"title": "Engagement overlay",
|
|
745
|
+
"description": "One request for a whole page of cards, read by every card in it. A storefront's feed and SERP are drawn from the search index, whose stored document can carry neither a per-reader flag nor a fast-moving counter — so without this scope the already-seen rows and the saved ones are indistinguishable from the rest, on exactly the two screens the state exists for. The container opens a scope with the ids it just rendered, GET /listings/engagement/ answers once, and each card looks itself up and lays the entry over its own row. No scope, a missing id, a read in flight and a read that FAILED are all the same silent no-op: the card draws from its row and the grid keeps working.",
|
|
746
|
+
"component": "ListingEngagementScope",
|
|
747
|
+
"covers": [
|
|
748
|
+
"ListingEngagementContext",
|
|
749
|
+
"ListingsProvider"
|
|
750
|
+
],
|
|
751
|
+
"tokens": [
|
|
752
|
+
"text-muted"
|
|
753
|
+
],
|
|
754
|
+
"variants": [
|
|
755
|
+
"default",
|
|
756
|
+
"unscoped",
|
|
757
|
+
"visitor",
|
|
758
|
+
"failed"
|
|
759
|
+
],
|
|
760
|
+
"source": "demo/ListingEngagementScope.demo.tsx"
|
|
761
|
+
},
|
|
716
762
|
{
|
|
717
763
|
"id": "listings.favorites",
|
|
718
764
|
"title": "Favourites",
|
|
@@ -869,6 +915,8 @@
|
|
|
869
915
|
"error.400.invalid_ad_id",
|
|
870
916
|
"error.400.listing_feature_not_allowed",
|
|
871
917
|
"error.400.listing_invalid_status_filter",
|
|
918
|
+
"error.400.listing_location_required",
|
|
919
|
+
"error.400.listing_zero_price_not_allowed",
|
|
872
920
|
"error.400.publish_validation_failed",
|
|
873
921
|
"error.400.validation_error",
|
|
874
922
|
"error.400.verification_failed",
|
|
@@ -983,6 +1031,7 @@
|
|
|
983
1031
|
"listings.detail.stock",
|
|
984
1032
|
"listings.detail.take_down",
|
|
985
1033
|
"listings.detail.unreadable_features",
|
|
1034
|
+
"listings.detail.views",
|
|
986
1035
|
"listings.detail.withdrawn",
|
|
987
1036
|
"listings.error.unknown",
|
|
988
1037
|
"listings.favorites.empty",
|
|
@@ -1051,6 +1100,7 @@
|
|
|
1051
1100
|
"Favorites",
|
|
1052
1101
|
"IMAGES_FIELD",
|
|
1053
1102
|
"LISTINGS_ELEVATION_ACTIONS",
|
|
1103
|
+
"LISTINGS_ENGAGEMENT_BATCH_LIMIT",
|
|
1054
1104
|
"LISTINGS_ERRORS",
|
|
1055
1105
|
"LISTINGS_ERROR_CODES",
|
|
1056
1106
|
"LISTINGS_FLOWS",
|
|
@@ -1064,6 +1114,8 @@
|
|
|
1064
1114
|
"LOCATION_FIELD",
|
|
1065
1115
|
"ListingComposer",
|
|
1066
1116
|
"ListingDetail",
|
|
1117
|
+
"ListingEngagementContext",
|
|
1118
|
+
"ListingEngagementScope",
|
|
1067
1119
|
"ListingsProvider",
|
|
1068
1120
|
"ListingsRuntimeContext",
|
|
1069
1121
|
"MODERATION_STATUSES",
|
|
@@ -1085,6 +1137,8 @@
|
|
|
1085
1137
|
"draftValuesFromWire",
|
|
1086
1138
|
"droppedFeatureSlugs",
|
|
1087
1139
|
"emptyDraftValues",
|
|
1140
|
+
"engagementFor",
|
|
1141
|
+
"engagementIds",
|
|
1088
1142
|
"envelopeFieldErrors",
|
|
1089
1143
|
"explainListingsError",
|
|
1090
1144
|
"failedResults",
|
|
@@ -1094,10 +1148,12 @@
|
|
|
1094
1148
|
"featuresFromDaoList",
|
|
1095
1149
|
"flowEndpoints",
|
|
1096
1150
|
"isBatchValid",
|
|
1151
|
+
"isListingViewed",
|
|
1097
1152
|
"isPubliclyVisible",
|
|
1098
1153
|
"lifecycleCaption",
|
|
1099
1154
|
"listingFieldErrors",
|
|
1100
1155
|
"listingStatusView",
|
|
1156
|
+
"listingViewCount",
|
|
1101
1157
|
"listingsErrorBundleEn",
|
|
1102
1158
|
"listingsI18nBundleEn",
|
|
1103
1159
|
"listingsQueryKeys",
|
|
@@ -1120,6 +1176,8 @@
|
|
|
1120
1176
|
"useCreateDraft",
|
|
1121
1177
|
"useDeleteListing",
|
|
1122
1178
|
"useElevatableMandateGate",
|
|
1179
|
+
"useEngagedListing",
|
|
1180
|
+
"useEngagementOverlay",
|
|
1123
1181
|
"useFavoriteListing",
|
|
1124
1182
|
"useFavoriteToggle",
|
|
1125
1183
|
"useFavorites",
|
|
@@ -1128,6 +1186,9 @@
|
|
|
1128
1186
|
"useListingCards",
|
|
1129
1187
|
"useListingComposer",
|
|
1130
1188
|
"useListingDetail",
|
|
1189
|
+
"useListingEngagement",
|
|
1190
|
+
"useListingEngagementOverlay",
|
|
1191
|
+
"useListingEngagementScope",
|
|
1131
1192
|
"useListingStatus",
|
|
1132
1193
|
"useListingsAnalytics",
|
|
1133
1194
|
"useListingsApi",
|
|
@@ -1138,7 +1199,8 @@
|
|
|
1138
1199
|
"useMyListings",
|
|
1139
1200
|
"usePublishListing",
|
|
1140
1201
|
"useSaveDraft",
|
|
1141
|
-
"useValidateDraft"
|
|
1202
|
+
"useValidateDraft",
|
|
1203
|
+
"withEngagement"
|
|
1142
1204
|
],
|
|
1143
1205
|
"types": [
|
|
1144
1206
|
"ComposeStage",
|
|
@@ -1165,6 +1227,11 @@
|
|
|
1165
1227
|
"ListingDraftLimits",
|
|
1166
1228
|
"ListingDraftPatch",
|
|
1167
1229
|
"ListingDraftValues",
|
|
1230
|
+
"ListingEngagement",
|
|
1231
|
+
"ListingEngagementBag",
|
|
1232
|
+
"ListingEngagementBatch",
|
|
1233
|
+
"ListingEngagementFields",
|
|
1234
|
+
"ListingEngagementScopeProps",
|
|
1168
1235
|
"ListingFeatureDao",
|
|
1169
1236
|
"ListingFeatureDaoUnion",
|
|
1170
1237
|
"ListingFeatureType",
|
|
@@ -1204,6 +1271,7 @@
|
|
|
1204
1271
|
"UseFavoritesOptions",
|
|
1205
1272
|
"UseListingComposerOptions",
|
|
1206
1273
|
"UseListingDetailOptions",
|
|
1274
|
+
"UseListingEngagementOverlayOptions",
|
|
1207
1275
|
"UseMyListingsOptions"
|
|
1208
1276
|
]
|
|
1209
1277
|
}
|
package/nav-manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stapel/listings-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Headless React pair for stapel-listings: a typed client over the draft twin, the composer that turns a category schema into a submittable listing, and the owner's dashboard. Two independent axes are rendered as two — lifecycle `status` decides visibility and `moderation_status` decides nothing about it, so an edit to a live listing stays live and says 'under review' instead of vanishing. Feature values are drawn and mirrored by @stapel/attributes-react; photos arrive as an injected upload bag whose `refs` ARE `images_draft` and whose `settled` gates the submit; a publish refusal is routed onto the control that caused it by slug. Zero visual opinion in the main entry; an opt-in /default subpath ships the antd skin.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
],
|
|
49
49
|
"size-limit": [
|
|
50
50
|
{
|
|
51
|
-
"name": "index — the headless pair (client + status axes + draft model + hooks): no antd. Raised 11 -> 12 KB for the elevation gate (useElevatableMandateGate + core's useElevation): the anonymous arm can now mint an identity for a named action instead of refusing, and that is a state hook the gate did not previously carry. Raised 11.5 -> 12 KB for envelopeFieldErrors and its API-field -> control table: a save-draft/create 400 names its field in the envelope rather than in a publish batch, and without the table the refusal had nowhere to land — which is how one over-precise coordinate painted two unreadable banners and left every control clean. Thirty-six bytes of routing table is what a legible refusal costs. Raised 12 -> 13 KB for two seams the mobile wave needed in the HEADLESS half: the category-feature copy source in model/features.ts (the option table a stored select does not carry, so a published listing prints \"Second-hand\" and not \"b-u\"), and firstUnsatisfied (the field a \"10 required details are still empty\" count actually points at, in the order the form asks). Both are model, not skin: a headless host renders its own controls and needs the same two answers",
|
|
51
|
+
"name": "index — the headless pair (client + status axes + draft model + hooks): no antd. Raised 11 -> 12 KB for the elevation gate (useElevatableMandateGate + core's useElevation): the anonymous arm can now mint an identity for a named action instead of refusing, and that is a state hook the gate did not previously carry. Raised 11.5 -> 12 KB for envelopeFieldErrors and its API-field -> control table: a save-draft/create 400 names its field in the envelope rather than in a publish batch, and without the table the refusal had nowhere to land — which is how one over-precise coordinate painted two unreadable banners and left every control clean. Thirty-six bytes of routing table is what a legible refusal costs. Raised 12 -> 13 KB for two seams the mobile wave needed in the HEADLESS half: the category-feature copy source in model/features.ts (the option table a stored select does not carry, so a published listing prints \"Second-hand\" and not \"b-u\"), and firstUnsatisfied (the field a \"10 required details are still empty\" count actually points at, in the order the form asks). Both are model, not skin: a headless host renders its own controls and needs the same two answers. Raised 13 -> 14 KB (measured 12.91) for the engagement overlay: a batch read, its normalized key, and the scope that runs it once for a page and hands every card its own entry. It is a whole new READ rather than a flag — and it is the read that makes the viewed/saved state visible at all on a feed and a SERP, which come from the search index and carry no per-viewer field. Landing at 12.91 of 13 would have been a budget passed by rounding",
|
|
52
52
|
"path": "dist/index.js",
|
|
53
|
-
"limit": "
|
|
53
|
+
"limit": "14 KB"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
|
-
"name": "default — the antd skin (three cards, detail, composer, dashboard, the named slots and the delete confirmation) must stay out of the main bundle. Raised 17 -> 19 KB for the mobile wave: <ListingSerpCard> (photo strip + price-first block + action rail), <ListingFeedCard> and <FeedGrid> are two more card surfaces on the same shared target/favourite/photo parts, not three copies of one Raised 19 -> 20 KB for the desktop wave of the detail page: layout=\"split\" is a second ASSEMBLY of the money page (the sticky buy column, the two-column spec table, the widened measure) over the same parts, and GateReasonPopover is the blocked heart's third volume — the reason and the door disclosed on interaction, with the visually-hidden copy that keeps the refusal in the accessibility tree. Both are geometry and wiring, not new widgets",
|
|
56
|
+
"name": "default — the antd skin (three cards, detail, composer, dashboard, the named slots and the delete confirmation) must stay out of the main bundle. Raised 17 -> 19 KB for the mobile wave: <ListingSerpCard> (photo strip + price-first block + action rail), <ListingFeedCard> and <FeedGrid> are two more card surfaces on the same shared target/favourite/photo parts, not three copies of one Raised 19 -> 20 KB for the desktop wave of the detail page: layout=\"split\" is a second ASSEMBLY of the money page (the sticky buy column, the two-column spec table, the widened measure) over the same parts, and GateReasonPopover is the blocked heart's third volume — the reason and the door disclosed on interaction, with the visually-hidden copy that keeps the refusal in the accessibility tree. Both are geometry and wiring, not new widgets. Raised 20 -> 21 KB (measured 20.22) for the engagement wave, which is four small things and no new component: the disclosure became a controlled open/close so a pointer that hovers and then clicks does not shut the only explanation the control has; the heart draws a filled accent when saved and states a failed save through the shared ErrorAlert; an already-seen card dims through one rule in the sheet all three cards already hoist; and the listing page gained a view-count row. 216 bytes is what a control that reports its own outcome costs",
|
|
57
57
|
"path": "dist/default/index.js",
|
|
58
|
-
"limit": "
|
|
58
|
+
"limit": "21 KB"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"name": "i18n/ru — the Russian bundle is opt-in, not carried by the main entry",
|
|
@@ -102,13 +102,13 @@
|
|
|
102
102
|
"size-limit": "^11.2.0",
|
|
103
103
|
"typescript": "^5.8.3",
|
|
104
104
|
"vitest": "^3.2.4",
|
|
105
|
-
"@stapel/attributes-react": "^0.
|
|
106
|
-
"@stapel/currencies-react": "^0.3.0",
|
|
105
|
+
"@stapel/attributes-react": "^0.13.0",
|
|
107
106
|
"@stapel/core": "^0.23.1",
|
|
107
|
+
"@stapel/currencies-react": "^0.3.0",
|
|
108
108
|
"@stapel/image": "^0.4.2",
|
|
109
109
|
"@stapel/showcase": "^0.3.0",
|
|
110
110
|
"@stapel/tokens": "^0.7.0",
|
|
111
|
-
"@stapel/tokens-antd": "^0.
|
|
111
|
+
"@stapel/tokens-antd": "^0.16.0"
|
|
112
112
|
},
|
|
113
113
|
"engines": {
|
|
114
114
|
"node": ">=22"
|
|
@@ -45,15 +45,25 @@ export interface paths {
|
|
|
45
45
|
cookie?: never;
|
|
46
46
|
};
|
|
47
47
|
/**
|
|
48
|
-
* @description
|
|
48
|
+
* @description Read one listing — and count the open.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
50
|
+
* A GET with a side effect, deliberately, because "opening a listing"
|
|
51
|
+
* IS the event and a storefront that had to POST it separately would
|
|
52
|
+
* report a different number than the page it drew (and would stop
|
|
53
|
+
* reporting at all the moment a client forgot the second call).
|
|
54
|
+
*
|
|
55
|
+
* The side effect is bounded on both ends. It cannot lie: the owner's
|
|
56
|
+
* own opens and unattributable ones are refused inside
|
|
57
|
+
* ``record_view``. And it cannot become a write per read: every repeat
|
|
58
|
+
* open inside ``VIEW_DEDUP_WINDOW_SECONDS`` is one cache hit and no
|
|
59
|
+
* query. The response is therefore safe to give a private cache and
|
|
60
|
+
* must never be given a shared one — which is the pre-existing rule
|
|
61
|
+
* for an authenticated read anyway.
|
|
62
|
+
*
|
|
63
|
+
* ``viewed`` is annotated BEFORE the recording, so the open that first
|
|
64
|
+
* sees a listing answers ``false`` and the next one answers ``true``.
|
|
65
|
+
* A card that greyed itself out on the very read that discovered it
|
|
66
|
+
* would be telling the reader they had already been here.
|
|
57
67
|
*
|
|
58
68
|
* **Permissions:** `IsAuthenticatedOrReadOnly`
|
|
59
69
|
*/
|
|
@@ -308,6 +318,32 @@ export interface paths {
|
|
|
308
318
|
patch?: never;
|
|
309
319
|
trace?: never;
|
|
310
320
|
};
|
|
321
|
+
"/listings/api/v1/listings/engagement/": {
|
|
322
|
+
parameters: {
|
|
323
|
+
query?: never;
|
|
324
|
+
header?: never;
|
|
325
|
+
path?: never;
|
|
326
|
+
cookie?: never;
|
|
327
|
+
};
|
|
328
|
+
/**
|
|
329
|
+
* @description The per-viewer overlay for a page of cards, in one call.
|
|
330
|
+
*
|
|
331
|
+
* ``AllowAny`` on purpose: ``view_count`` is public, and an anonymous
|
|
332
|
+
* caller gets it with both per-viewer flags answering ``null``. A
|
|
333
|
+
* storefront therefore makes the same request whether or not anyone is
|
|
334
|
+
* signed in, and a guest's grid is not a second code path.
|
|
335
|
+
*
|
|
336
|
+
* **Permissions:** `AllowAny`
|
|
337
|
+
*/
|
|
338
|
+
get: operations["listings_api_v1_listings_engagement_retrieve"];
|
|
339
|
+
put?: never;
|
|
340
|
+
post?: never;
|
|
341
|
+
delete?: never;
|
|
342
|
+
options?: never;
|
|
343
|
+
head?: never;
|
|
344
|
+
patch?: never;
|
|
345
|
+
trace?: never;
|
|
346
|
+
};
|
|
311
347
|
"/listings/api/v1/listings/my/counters/": {
|
|
312
348
|
parameters: {
|
|
313
349
|
query?: never;
|
|
@@ -522,8 +558,8 @@ export interface components {
|
|
|
522
558
|
favorited: boolean;
|
|
523
559
|
listing_id: number;
|
|
524
560
|
};
|
|
525
|
-
FeatureDao: components["schemas"]["
|
|
526
|
-
FeatureDto: components["schemas"]["
|
|
561
|
+
FeatureDao: components["schemas"]["RefSelectDao"] | components["schemas"]["IntDao"] | components["schemas"]["FloatDao"] | components["schemas"]["StringDao"] | components["schemas"]["BoolDao"] | components["schemas"]["HexColorDao"] | components["schemas"]["SelectDao"] | components["schemas"]["DateDao"] | components["schemas"]["HeaderDao"] | components["schemas"]["HierarchicalSelectDao"] | components["schemas"]["ConvertibleUnitDao"] | components["schemas"]["RefHierarchicalSelectDao"] | components["schemas"]["GroupDao"];
|
|
562
|
+
FeatureDto: components["schemas"]["RefSelectDto"] | components["schemas"]["IntDto"] | components["schemas"]["FloatDto"] | components["schemas"]["StringDto"] | components["schemas"]["BoolDto"] | components["schemas"]["HexColorDto"] | components["schemas"]["SelectDto"] | components["schemas"]["DateDto"] | components["schemas"]["HeaderDto"] | components["schemas"]["HierarchicalSelectDto"] | components["schemas"]["ConvertibleUnitDto"] | components["schemas"]["RefHierarchicalSelectDto"] | components["schemas"]["GroupDto"];
|
|
527
563
|
/** @description Serializer for FeatureValidationResult. */
|
|
528
564
|
FeatureValidationResult: {
|
|
529
565
|
id?: unknown;
|
|
@@ -738,7 +774,7 @@ export interface components {
|
|
|
738
774
|
readonly id: number;
|
|
739
775
|
title?: string;
|
|
740
776
|
/** Format: decimal */
|
|
741
|
-
price?: string;
|
|
777
|
+
price?: string | null;
|
|
742
778
|
/** Format: decimal */
|
|
743
779
|
price_base?: string | null;
|
|
744
780
|
currency?: string;
|
|
@@ -784,6 +820,8 @@ export interface components {
|
|
|
784
820
|
stock_quantity?: number | null;
|
|
785
821
|
status?: components["schemas"]["StatusD41Enum"];
|
|
786
822
|
readonly is_favorited: boolean | null;
|
|
823
|
+
readonly viewed: boolean | null;
|
|
824
|
+
readonly view_count: number;
|
|
787
825
|
};
|
|
788
826
|
/** @description Full listing detail. */
|
|
789
827
|
ListingDetail: {
|
|
@@ -795,7 +833,7 @@ export interface components {
|
|
|
795
833
|
description?: string;
|
|
796
834
|
language?: string;
|
|
797
835
|
/** Format: decimal */
|
|
798
|
-
price?: string;
|
|
836
|
+
price?: string | null;
|
|
799
837
|
/** Format: decimal */
|
|
800
838
|
price_base?: string | null;
|
|
801
839
|
currency?: string;
|
|
@@ -868,6 +906,8 @@ export interface components {
|
|
|
868
906
|
/** Format: date-time */
|
|
869
907
|
readonly updated_at: string;
|
|
870
908
|
readonly is_favorited: boolean | null;
|
|
909
|
+
readonly viewed: boolean | null;
|
|
910
|
+
readonly view_count: number;
|
|
871
911
|
};
|
|
872
912
|
/**
|
|
873
913
|
* @description Create/update the draft twin fields.
|
|
@@ -907,6 +947,30 @@ export interface components {
|
|
|
907
947
|
/** Format: date-time */
|
|
908
948
|
readonly updated_at: string;
|
|
909
949
|
};
|
|
950
|
+
/**
|
|
951
|
+
* @description The per-viewer overlay for ONE listing in a batch read.
|
|
952
|
+
*
|
|
953
|
+
* Exists because a storefront's grid does not come from this module: the
|
|
954
|
+
* SERP is served by the search index, whose stored card can carry neither a
|
|
955
|
+
* flag that differs per reader nor a counter that moves faster than a
|
|
956
|
+
* document re-indexed on a listing event. So the grid draws the card from
|
|
957
|
+
* search and asks HERE, once for the whole page, for the three things that
|
|
958
|
+
* are about the person looking.
|
|
959
|
+
*/
|
|
960
|
+
ListingEngagement: {
|
|
961
|
+
/** @description Distinct viewers who have opened this listing. Public — it is the same number for every reader. */
|
|
962
|
+
view_count: number;
|
|
963
|
+
/** @description Whether the CALLER has opened this listing before. `null` for an anonymous caller: nothing is remembered for a stranger, and `false` would be a claim rather than an absence. */
|
|
964
|
+
viewed: boolean | null;
|
|
965
|
+
/** @description Whether the CALLER has favorited it. `null` for anonymous, same reason. */
|
|
966
|
+
is_favorited: boolean | null;
|
|
967
|
+
};
|
|
968
|
+
/** @description ``{listing id: overlay}``. An id with no listing is simply absent. */
|
|
969
|
+
ListingEngagementBatch: {
|
|
970
|
+
items: {
|
|
971
|
+
[key: string]: components["schemas"]["ListingEngagement"];
|
|
972
|
+
};
|
|
973
|
+
};
|
|
910
974
|
/** @description Lightweight status view (mirrors the listings.status comm Function). */
|
|
911
975
|
ListingStatus: {
|
|
912
976
|
status: components["schemas"]["StatusD41Enum"];
|
|
@@ -954,7 +1018,7 @@ export interface components {
|
|
|
954
1018
|
readonly id: number;
|
|
955
1019
|
title?: string;
|
|
956
1020
|
/** Format: decimal */
|
|
957
|
-
price?: string;
|
|
1021
|
+
price?: string | null;
|
|
958
1022
|
/** Format: decimal */
|
|
959
1023
|
price_base?: string | null;
|
|
960
1024
|
currency?: string;
|
|
@@ -1000,6 +1064,8 @@ export interface components {
|
|
|
1000
1064
|
stock_quantity?: number | null;
|
|
1001
1065
|
status?: components["schemas"]["StatusD41Enum"];
|
|
1002
1066
|
readonly is_favorited: boolean | null;
|
|
1067
|
+
readonly viewed: boolean | null;
|
|
1068
|
+
readonly view_count: number;
|
|
1003
1069
|
moderation_status?: components["schemas"]["ModerationStatusEnum"];
|
|
1004
1070
|
title_draft?: string;
|
|
1005
1071
|
/** Format: decimal */
|
|
@@ -1641,6 +1707,28 @@ export interface operations {
|
|
|
1641
1707
|
};
|
|
1642
1708
|
};
|
|
1643
1709
|
};
|
|
1710
|
+
listings_api_v1_listings_engagement_retrieve: {
|
|
1711
|
+
parameters: {
|
|
1712
|
+
query: {
|
|
1713
|
+
/** @description Listing ids, comma-separated or the parameter repeated. Capped at ENGAGEMENT_BATCH_LIMIT ids per call — one page of cards, not a crawl of the board. */
|
|
1714
|
+
ids: string[];
|
|
1715
|
+
};
|
|
1716
|
+
header?: never;
|
|
1717
|
+
path?: never;
|
|
1718
|
+
cookie?: never;
|
|
1719
|
+
};
|
|
1720
|
+
requestBody?: never;
|
|
1721
|
+
responses: {
|
|
1722
|
+
200: {
|
|
1723
|
+
headers: {
|
|
1724
|
+
[name: string]: unknown;
|
|
1725
|
+
};
|
|
1726
|
+
content: {
|
|
1727
|
+
"application/json": components["schemas"]["ListingEngagementBatch"];
|
|
1728
|
+
};
|
|
1729
|
+
};
|
|
1730
|
+
};
|
|
1731
|
+
};
|
|
1644
1732
|
listings_api_v1_listings_my_counters_retrieve: {
|
|
1645
1733
|
parameters: {
|
|
1646
1734
|
query?: never;
|
package/src/api/listingsApi.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
FavoriteToggleResponse,
|
|
5
5
|
ListingActionResponse,
|
|
6
6
|
ListingDetail,
|
|
7
|
+
ListingEngagementBatch,
|
|
7
8
|
ListingDraft,
|
|
8
9
|
ListingDraftPatch,
|
|
9
10
|
ListingPageParams,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
PublishResponse,
|
|
16
17
|
} from "./types.js";
|
|
17
18
|
import type { ValidationBatchResult } from "@stapel/attributes-react";
|
|
19
|
+
import { engagementIds } from "./types.js";
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* The pair's typed operation surface — one method per stapel-listings endpoint
|
|
@@ -123,6 +125,33 @@ export interface ListingsApi {
|
|
|
123
125
|
options?: { readonly signal?: AbortSignal }
|
|
124
126
|
): Promise<PaginatedListingCards>;
|
|
125
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The per-viewer OVERLAY for a whole page of cards, in one call.
|
|
130
|
+
*
|
|
131
|
+
* `{listing id: {view_count, viewed, is_favorited}}`, with an id that has
|
|
132
|
+
* no listing simply absent. This is the endpoint that makes the engagement
|
|
133
|
+
* flags visible on the two surfaces that matter: a storefront's feed and
|
|
134
|
+
* its SERP are served by the SEARCH index, whose stored card cannot hold a
|
|
135
|
+
* flag that differs per reader or a counter that moves faster than a
|
|
136
|
+
* re-index, so the grid draws the card from search and asks HERE for the
|
|
137
|
+
* three things that are about the person looking.
|
|
138
|
+
*
|
|
139
|
+
* `AllowAny` upstream, deliberately: `view_count` is public and both
|
|
140
|
+
* per-viewer flags answer `null` for a guest, so a storefront makes the
|
|
141
|
+
* same request signed in or not and a guest's grid is not a second code
|
|
142
|
+
* path.
|
|
143
|
+
*
|
|
144
|
+
* Ids are normalized by `engagementIds` before they get here — sorted,
|
|
145
|
+
* de-duplicated and capped at `LISTINGS_ENGAGEMENT_BATCH_LIMIT`, because
|
|
146
|
+
* the server TRUNCATES a longer list rather than refusing it and a short
|
|
147
|
+
* answer to a long question is the kind of absence this pair does not
|
|
148
|
+
* render.
|
|
149
|
+
*/
|
|
150
|
+
engagement(
|
|
151
|
+
ids: readonly number[],
|
|
152
|
+
options?: { readonly signal?: AbortSignal }
|
|
153
|
+
): Promise<ListingEngagementBatch>;
|
|
154
|
+
|
|
126
155
|
/**
|
|
127
156
|
* Start a draft. `category_id` is the only required member: the server
|
|
128
157
|
* forces `owner=request.user` and `status=draft` in `perform_create`, so a
|
|
@@ -254,6 +283,16 @@ export function createListingsApi(client: StapelClient): ListingsApi {
|
|
|
254
283
|
...signal(options),
|
|
255
284
|
}),
|
|
256
285
|
|
|
286
|
+
// One comma-separated value rather than a repeated parameter, for the
|
|
287
|
+
// reason `statusQuery` gives: both spellings are accepted upstream
|
|
288
|
+
// (`getlist("ids")` then splits on commas), and this one survives every
|
|
289
|
+
// `query` serializer a host's own StapelClient might carry.
|
|
290
|
+
engagement: (ids, options) =>
|
|
291
|
+
client.get(`${COLLECTION}engagement/`, {
|
|
292
|
+
query: { ids: engagementIds(ids).join(",") },
|
|
293
|
+
...signal(options),
|
|
294
|
+
}),
|
|
295
|
+
|
|
257
296
|
createDraft: (body) => client.post(COLLECTION, body),
|
|
258
297
|
|
|
259
298
|
saveDraft: (id, body) => client.post(`${listingPath(id)}save-draft/`, body),
|