@stapel/listings-react 0.9.1 → 0.11.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/default/FeedGrid.d.ts +42 -0
  3. package/dist/default/FeedGrid.d.ts.map +1 -0
  4. package/dist/default/FeedGrid.js +23 -0
  5. package/dist/default/FeedGrid.js.map +1 -0
  6. package/dist/default/ListingCard.d.ts +31 -0
  7. package/dist/default/ListingCard.d.ts.map +1 -1
  8. package/dist/default/ListingCard.js +12 -4
  9. package/dist/default/ListingCard.js.map +1 -1
  10. package/dist/default/ListingComposerPage.d.ts.map +1 -1
  11. package/dist/default/ListingComposerPage.js +10 -4
  12. package/dist/default/ListingComposerPage.js.map +1 -1
  13. package/dist/default/ListingFeedCard.d.ts +77 -0
  14. package/dist/default/ListingFeedCard.d.ts.map +1 -0
  15. package/dist/default/ListingFeedCard.js +75 -0
  16. package/dist/default/ListingFeedCard.js.map +1 -0
  17. package/dist/default/ListingSerpCard.d.ts +114 -0
  18. package/dist/default/ListingSerpCard.d.ts.map +1 -0
  19. package/dist/default/ListingSerpCard.js +72 -0
  20. package/dist/default/ListingSerpCard.js.map +1 -0
  21. package/dist/default/favorite.d.ts +49 -0
  22. package/dist/default/favorite.d.ts.map +1 -0
  23. package/dist/default/favorite.js +25 -0
  24. package/dist/default/favorite.js.map +1 -0
  25. package/dist/default/icons.d.ts +17 -3
  26. package/dist/default/icons.d.ts.map +1 -1
  27. package/dist/default/icons.js +13 -1
  28. package/dist/default/icons.js.map +1 -1
  29. package/dist/default/index.d.ts +17 -1
  30. package/dist/default/index.d.ts.map +1 -1
  31. package/dist/default/index.js +14 -1
  32. package/dist/default/index.js.map +1 -1
  33. package/dist/headless/ListingComposer.d.ts.map +1 -1
  34. package/dist/headless/ListingComposer.js +14 -6
  35. package/dist/headless/ListingComposer.js.map +1 -1
  36. package/dist/i18n/es.d.ts.map +1 -1
  37. package/dist/i18n/es.js +4 -0
  38. package/dist/i18n/es.js.map +1 -1
  39. package/dist/i18n/keys.d.ts +11 -0
  40. package/dist/i18n/keys.d.ts.map +1 -1
  41. package/dist/i18n/keys.js +15 -0
  42. package/dist/i18n/keys.js.map +1 -1
  43. package/dist/i18n/ru.d.ts.map +1 -1
  44. package/dist/i18n/ru.js +4 -0
  45. package/dist/i18n/ru.js.map +1 -1
  46. package/dist/index.d.ts +1 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +1 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/model/validation.d.ts +20 -0
  51. package/dist/model/validation.d.ts.map +1 -1
  52. package/dist/model/validation.js +63 -1
  53. package/dist/model/validation.js.map +1 -1
  54. package/llms.txt +3 -1
  55. package/manifest.json +47 -1
  56. package/nav-manifest.json +1 -1
  57. package/package.json +10 -10
  58. package/src/analytics/generated/events.json +1 -1
  59. package/src/default/FeedGrid.tsx +70 -0
  60. package/src/default/ListingCard.tsx +21 -5
  61. package/src/default/ListingComposerPage.tsx +18 -3
  62. package/src/default/ListingFeedCard.tsx +232 -0
  63. package/src/default/ListingSerpCard.tsx +374 -0
  64. package/src/default/favorite.tsx +91 -0
  65. package/src/default/icons.tsx +37 -3
  66. package/src/default/index.ts +24 -1
  67. package/src/headless/ListingComposer.tsx +19 -6
  68. package/src/i18n/es.ts +4 -0
  69. package/src/i18n/keys.ts +15 -0
  70. package/src/i18n/ru.ts +4 -0
  71. package/src/index.ts +2 -0
  72. package/src/model/validation.ts +64 -1
@@ -93,6 +93,31 @@ export const TITLE_FIELD = "title";
93
93
  export const PRICE_FIELD = "price";
94
94
  export const IMAGES_FIELD = "images";
95
95
  export const CATEGORY_FIELD = "category_id";
96
+ /** The coordinate pair, as one control: a latitude alone is not a place. */
97
+ export const LOCATION_FIELD = "location";
98
+
99
+ /**
100
+ * The API's own field names → the control that holds them.
101
+ *
102
+ * The draft serializer writes `*_draft` columns, and the two coordinates are
103
+ * one field on screen. Without this table a per-field 400 has nowhere to land
104
+ * and becomes a banner nobody can act on — which is the whole of blocker C2's
105
+ * second half: a save refused for `lat_draft` painted "Validation error" over
106
+ * the footer while the location control, the only thing the person could have
107
+ * changed, stayed clean.
108
+ */
109
+ const CONTROL_OF_API_FIELD: Readonly<Record<string, string>> = {
110
+ title_draft: TITLE_FIELD,
111
+ description_draft: DESCRIPTION_FIELD,
112
+ price_draft: PRICE_FIELD,
113
+ images_draft: IMAGES_FIELD,
114
+ category_id: CATEGORY_FIELD,
115
+ lat_draft: LOCATION_FIELD,
116
+ lon_draft: LOCATION_FIELD,
117
+ location_id_draft: LOCATION_FIELD,
118
+ location_label_draft: LOCATION_FIELD,
119
+ stock_quantity: "stockQuantity",
120
+ };
96
121
 
97
122
  /** A client-side refusal. `status: 0` on purpose: a rule this build applied
98
123
  * must never be indistinguishable from one that came over the wire. */
@@ -162,7 +187,7 @@ export function mirrorListingFields(
162
187
  const hasLat = lat !== null && lat.length > 0;
163
188
  const hasLon = lon !== null && lon.length > 0;
164
189
  if (hasLat !== hasLon) {
165
- out["location"] = mirrored(LISTINGS_I18N_KEYS.composeGeoIncomplete);
190
+ out[LOCATION_FIELD] = mirrored(LISTINGS_I18N_KEYS.composeGeoIncomplete);
166
191
  }
167
192
 
168
193
  return out;
@@ -244,6 +269,44 @@ export function publishRefusal(thrown: unknown): PublishRefusal {
244
269
  };
245
270
  }
246
271
 
272
+ /**
273
+ * A per-field refusal in the ORDINARY envelope → the control it belongs to.
274
+ *
275
+ * `save-draft` and `create` do not answer a batch: DRF's field validation
276
+ * raises, and stapel-core's handler folds it into one envelope carrying
277
+ * `params.field` — the field name, and often the ONLY thing in the response
278
+ * that says what went wrong, because a DRF code the registry does not know
279
+ * (`max_decimal_places`, say) collapses to the generic
280
+ * `error.400.validation_error`, whose sentence is "Validation error" and
281
+ * nothing else.
282
+ *
283
+ * So the field name is the payload. Put on the control, it is the difference
284
+ * between a red banner the person cannot act on and a red field they can.
285
+ * Returns `{}` for anything that names no field, or names one this composer
286
+ * does not draw — a banner is right for those, and a refusal routed to a
287
+ * control that does not exist would vanish.
288
+ */
289
+ export function envelopeFieldErrors(
290
+ thrown: unknown
291
+ ): Readonly<Record<string, FlowError>> {
292
+ if (!isStapelApiError(thrown)) return {};
293
+ const named = thrown.params["field"];
294
+ if (typeof named !== "string" || named.length === 0) return {};
295
+ const control = CONTROL_OF_API_FIELD[named];
296
+ if (control === undefined) return {};
297
+ return {
298
+ [control]: {
299
+ code: thrown.code,
300
+ // `field` is the fleet's routing param; the control key is what a
301
+ // sentence should name, not the column the server happened to use.
302
+ params: { ...thrown.params, field: control },
303
+ status: thrown.status,
304
+ message: thrown.message,
305
+ language: thrown.language,
306
+ },
307
+ };
308
+ }
309
+
247
310
  /**
248
311
  * A server batch → refusals keyed by CONTROL.
249
312
  *