@stapel/listings-react 0.4.0 → 0.8.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 +180 -0
- package/dist/api/generated/schema.d.ts +94 -4
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/FavoritesPane.d.ts +40 -3
- package/dist/default/FavoritesPane.d.ts.map +1 -1
- package/dist/default/FavoritesPane.js +30 -18
- package/dist/default/FavoritesPane.js.map +1 -1
- package/dist/default/ListingCard.d.ts +81 -4
- package/dist/default/ListingCard.d.ts.map +1 -1
- package/dist/default/ListingCard.js +97 -27
- package/dist/default/ListingCard.js.map +1 -1
- package/dist/default/ListingComposerPage.d.ts +163 -23
- package/dist/default/ListingComposerPage.d.ts.map +1 -1
- package/dist/default/ListingComposerPage.js +85 -43
- package/dist/default/ListingComposerPage.js.map +1 -1
- package/dist/default/ListingDetailPane.d.ts +41 -4
- package/dist/default/ListingDetailPane.d.ts.map +1 -1
- package/dist/default/ListingDetailPane.js +38 -25
- package/dist/default/ListingDetailPane.js.map +1 -1
- package/dist/default/ListingPhoto.d.ts +13 -1
- package/dist/default/ListingPhoto.d.ts.map +1 -1
- package/dist/default/ListingPhoto.js +47 -9
- package/dist/default/ListingPhoto.js.map +1 -1
- package/dist/default/ListingPrice.d.ts +13 -0
- package/dist/default/ListingPrice.d.ts.map +1 -0
- package/dist/default/ListingPrice.js +60 -0
- package/dist/default/ListingPrice.js.map +1 -0
- package/dist/default/MyListingsPane.d.ts +7 -28
- package/dist/default/MyListingsPane.d.ts.map +1 -1
- package/dist/default/MyListingsPane.js +136 -67
- package/dist/default/MyListingsPane.js.map +1 -1
- package/dist/default/SignInLink.js +8 -7
- package/dist/default/SignInLink.js.map +1 -1
- package/dist/default/StatusTags.d.ts +16 -5
- package/dist/default/StatusTags.d.ts.map +1 -1
- package/dist/default/StatusTags.js +19 -13
- package/dist/default/StatusTags.js.map +1 -1
- package/dist/default/index.d.ts +15 -9
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +13 -6
- package/dist/default/index.js.map +1 -1
- package/dist/headless/Favorites.d.ts +6 -0
- package/dist/headless/Favorites.d.ts.map +1 -1
- package/dist/headless/Favorites.js +23 -7
- package/dist/headless/Favorites.js.map +1 -1
- package/dist/headless/ListingActions.d.ts +12 -0
- package/dist/headless/ListingActions.d.ts.map +1 -1
- package/dist/headless/ListingActions.js +3 -0
- package/dist/headless/ListingActions.js.map +1 -1
- package/dist/headless/ListingComposer.d.ts +23 -0
- package/dist/headless/ListingComposer.d.ts.map +1 -1
- package/dist/headless/ListingComposer.js +33 -7
- package/dist/headless/ListingComposer.js.map +1 -1
- package/dist/headless/ListingDetail.d.ts.map +1 -1
- package/dist/headless/ListingDetail.js +7 -5
- package/dist/headless/ListingDetail.js.map +1 -1
- package/dist/headless/useMandateGate.d.ts +33 -2
- package/dist/headless/useMandateGate.d.ts.map +1 -1
- package/dist/headless/useMandateGate.js +42 -4
- package/dist/headless/useMandateGate.js.map +1 -1
- package/dist/i18n/es.d.ts +6 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +23 -5
- 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 +6 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +3 -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 +30 -3
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +47 -7
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts +16 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +35 -5
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/model/draft.d.ts +9 -6
- package/dist/model/draft.d.ts.map +1 -1
- package/dist/model/draft.js +3 -1
- package/dist/model/draft.js.map +1 -1
- package/dist/model/features.d.ts +21 -7
- package/dist/model/features.d.ts.map +1 -1
- package/dist/model/features.js +26 -0
- package/dist/model/features.js.map +1 -1
- package/llms.txt +9 -8
- package/manifest.json +82 -31
- package/nav-manifest.json +1 -1
- package/package.json +19 -14
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +94 -4
- package/src/default/FavoritesPane.tsx +156 -82
- package/src/default/ListingCard.tsx +283 -116
- package/src/default/ListingComposerPage.tsx +403 -187
- package/src/default/ListingDetailPane.tsx +200 -98
- package/src/default/ListingPhoto.tsx +89 -16
- package/src/default/ListingPrice.tsx +77 -0
- package/src/default/MyListingsPane.tsx +286 -228
- package/src/default/SignInLink.tsx +4 -4
- package/src/default/StatusTags.tsx +36 -18
- package/src/default/index.ts +18 -7
- package/src/headless/Favorites.tsx +36 -7
- package/src/headless/ListingActions.tsx +19 -0
- package/src/headless/ListingComposer.tsx +56 -5
- package/src/headless/ListingDetail.tsx +12 -5
- package/src/headless/useMandateGate.ts +49 -5
- package/src/i18n/es.ts +29 -5
- package/src/i18n/generated/errors.es.gen.ts +1 -1
- package/src/i18n/generated/errors.gen.ts +3 -0
- package/src/i18n/generated/errors.json +7 -0
- package/src/i18n/generated/errors.ru.gen.ts +1 -1
- package/src/i18n/keys.ts +54 -7
- package/src/i18n/ru.ts +41 -5
- package/src/index.ts +5 -1
- package/src/model/draft.ts +12 -7
- package/src/model/features.ts +48 -7
- package/dist/default/ErrorAlert.d.ts +0 -9
- package/dist/default/ErrorAlert.d.ts.map +0 -1
- package/dist/default/ErrorAlert.js +0 -27
- package/dist/default/ErrorAlert.js.map +0 -1
- package/dist/default/theme.d.ts +0 -25
- package/dist/default/theme.d.ts.map +0 -1
- package/dist/default/theme.js +0 -40
- package/dist/default/theme.js.map +0 -1
- package/src/default/ErrorAlert.tsx +0 -46
- package/src/default/theme.tsx +0 -67
|
@@ -12,12 +12,21 @@ export interface paths {
|
|
|
12
12
|
/**
|
|
13
13
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
14
14
|
*
|
|
15
|
+
* Read and write live in one class, so the guest wall
|
|
16
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
17
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
18
|
+
* session must keep browsing and keep its favorites, and only the
|
|
19
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
20
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
21
|
+
* seller.
|
|
22
|
+
*
|
|
15
23
|
* **Permissions:** `IsAuthenticatedOrReadOnly`
|
|
16
24
|
*/
|
|
17
25
|
get: operations["listings_api_v1_listings_list"];
|
|
18
26
|
put?: never;
|
|
19
27
|
/**
|
|
20
|
-
* @description
|
|
28
|
+
* @description Open a draft — the first act of becoming a seller, so the guest
|
|
29
|
+
* wall stands here and not only at ``publish``.
|
|
21
30
|
*
|
|
22
31
|
* **Permissions:** `IsAuthenticatedOrReadOnly`
|
|
23
32
|
*/
|
|
@@ -38,6 +47,14 @@ export interface paths {
|
|
|
38
47
|
/**
|
|
39
48
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
40
49
|
*
|
|
50
|
+
* Read and write live in one class, so the guest wall
|
|
51
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
52
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
53
|
+
* session must keep browsing and keep its favorites, and only the
|
|
54
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
55
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
56
|
+
* seller.
|
|
57
|
+
*
|
|
41
58
|
* **Permissions:** `IsAuthenticatedOrReadOnly`
|
|
42
59
|
*/
|
|
43
60
|
get: operations["listings_api_v1_listings_retrieve"];
|
|
@@ -53,6 +70,14 @@ export interface paths {
|
|
|
53
70
|
/**
|
|
54
71
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
55
72
|
*
|
|
73
|
+
* Read and write live in one class, so the guest wall
|
|
74
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
75
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
76
|
+
* session must keep browsing and keep its favorites, and only the
|
|
77
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
78
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
79
|
+
* seller.
|
|
80
|
+
*
|
|
56
81
|
* **Permissions:** `IsAuthenticatedOrReadOnly`
|
|
57
82
|
*/
|
|
58
83
|
delete: operations["listings_api_v1_listings_destroy"];
|
|
@@ -61,6 +86,14 @@ export interface paths {
|
|
|
61
86
|
/**
|
|
62
87
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
63
88
|
*
|
|
89
|
+
* Read and write live in one class, so the guest wall
|
|
90
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
91
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
92
|
+
* session must keep browsing and keep its favorites, and only the
|
|
93
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
94
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
95
|
+
* seller.
|
|
96
|
+
*
|
|
64
97
|
* **Permissions:** `IsAuthenticatedOrReadOnly`
|
|
65
98
|
*/
|
|
66
99
|
patch: operations["listings_api_v1_listings_partial_update"];
|
|
@@ -78,6 +111,14 @@ export interface paths {
|
|
|
78
111
|
/**
|
|
79
112
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
80
113
|
*
|
|
114
|
+
* Read and write live in one class, so the guest wall
|
|
115
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
116
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
117
|
+
* session must keep browsing and keep its favorites, and only the
|
|
118
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
119
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
120
|
+
* seller.
|
|
121
|
+
*
|
|
81
122
|
* **Permissions:** `IsAuthenticated`
|
|
82
123
|
*/
|
|
83
124
|
post: operations["listings_api_v1_listings_archive_create"];
|
|
@@ -120,6 +161,14 @@ export interface paths {
|
|
|
120
161
|
/**
|
|
121
162
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
122
163
|
*
|
|
164
|
+
* Read and write live in one class, so the guest wall
|
|
165
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
166
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
167
|
+
* session must keep browsing and keep its favorites, and only the
|
|
168
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
169
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
170
|
+
* seller.
|
|
171
|
+
*
|
|
123
172
|
* **Permissions:** `IsAuthenticated`
|
|
124
173
|
*/
|
|
125
174
|
post: operations["listings_api_v1_listings_favorite_create"];
|
|
@@ -139,7 +188,8 @@ export interface paths {
|
|
|
139
188
|
get?: never;
|
|
140
189
|
put?: never;
|
|
141
190
|
/**
|
|
142
|
-
* @description
|
|
191
|
+
* @description Put a listing in front of buyers — the act the wall is named for:
|
|
192
|
+
* a seller nobody can reach again is not a seller.
|
|
143
193
|
*
|
|
144
194
|
* **Permissions:** `IsAuthenticated`
|
|
145
195
|
*/
|
|
@@ -181,6 +231,14 @@ export interface paths {
|
|
|
181
231
|
/**
|
|
182
232
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
183
233
|
*
|
|
234
|
+
* Read and write live in one class, so the guest wall
|
|
235
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
236
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
237
|
+
* session must keep browsing and keep its favorites, and only the
|
|
238
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
239
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
240
|
+
* seller.
|
|
241
|
+
*
|
|
184
242
|
* **Permissions:** `AllowAny`
|
|
185
243
|
*/
|
|
186
244
|
get: operations["listings_api_v1_listings_status_retrieve"];
|
|
@@ -204,6 +262,14 @@ export interface paths {
|
|
|
204
262
|
/**
|
|
205
263
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
206
264
|
*
|
|
265
|
+
* Read and write live in one class, so the guest wall
|
|
266
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
267
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
268
|
+
* session must keep browsing and keep its favorites, and only the
|
|
269
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
270
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
271
|
+
* seller.
|
|
272
|
+
*
|
|
207
273
|
* **Permissions:** `IsAuthenticated`
|
|
208
274
|
*/
|
|
209
275
|
post: operations["listings_api_v1_listings_unfavorite_create"];
|
|
@@ -223,6 +289,14 @@ export interface paths {
|
|
|
223
289
|
/**
|
|
224
290
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
225
291
|
*
|
|
292
|
+
* Read and write live in one class, so the guest wall
|
|
293
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
294
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
295
|
+
* session must keep browsing and keep its favorites, and only the
|
|
296
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
297
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
298
|
+
* seller.
|
|
299
|
+
*
|
|
226
300
|
* **Permissions:** `IsAuthenticated`
|
|
227
301
|
*/
|
|
228
302
|
get: operations["listings_api_v1_listings_validate_draft_retrieve"];
|
|
@@ -244,6 +318,14 @@ export interface paths {
|
|
|
244
318
|
/**
|
|
245
319
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
246
320
|
*
|
|
321
|
+
* Read and write live in one class, so the guest wall
|
|
322
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
323
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
324
|
+
* session must keep browsing and keep its favorites, and only the
|
|
325
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
326
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
327
|
+
* seller.
|
|
328
|
+
*
|
|
247
329
|
* **Permissions:** `IsAuthenticated`
|
|
248
330
|
*/
|
|
249
331
|
get: operations["listings_api_v1_listings_my_counters_retrieve"];
|
|
@@ -265,6 +347,14 @@ export interface paths {
|
|
|
265
347
|
/**
|
|
266
348
|
* @description Listings CRUD plus owner lifecycle actions and favorites.
|
|
267
349
|
*
|
|
350
|
+
* Read and write live in one class, so the guest wall
|
|
351
|
+
* (:func:`anonymous_write_refusal`, the ``ALLOW_ANONYMOUS_WRITES`` switch)
|
|
352
|
+
* is applied per ACTION and never as a class permission: an anonymous
|
|
353
|
+
* session must keep browsing and keep its favorites, and only the
|
|
354
|
+
* authorship actions — ``create``, ``update``/``partial_update``,
|
|
355
|
+
* ``save-draft``, ``publish`` — are the ones that turn a caller into a
|
|
356
|
+
* seller.
|
|
357
|
+
*
|
|
268
358
|
* **Permissions:** `IsAuthenticated`
|
|
269
359
|
*/
|
|
270
360
|
get: operations["listings_api_v1_listings_my_favorites_list"];
|
|
@@ -664,7 +754,7 @@ export interface components {
|
|
|
664
754
|
images_draft?: string[];
|
|
665
755
|
location_id_draft?: string;
|
|
666
756
|
location_label_draft?: string;
|
|
667
|
-
geohash_draft
|
|
757
|
+
readonly geohash_draft: string;
|
|
668
758
|
/** Format: decimal */
|
|
669
759
|
lat_draft?: string | null;
|
|
670
760
|
/** Format: decimal */
|
|
@@ -805,7 +895,7 @@ export interface components {
|
|
|
805
895
|
images_draft?: string[];
|
|
806
896
|
location_id_draft?: string;
|
|
807
897
|
location_label_draft?: string;
|
|
808
|
-
geohash_draft?: string;
|
|
898
|
+
readonly geohash_draft?: string;
|
|
809
899
|
/** Format: decimal */
|
|
810
900
|
lat_draft?: string | null;
|
|
811
901
|
/** Format: decimal */
|
|
@@ -6,19 +6,53 @@
|
|
|
6
6
|
* "You have not saved anything yet" and "we could not load your favourites"
|
|
7
7
|
* are the pair a `data ?? []` would have merged, and merging them is exactly
|
|
8
8
|
* the substitution that cost the fleet an incident (spec §7.4).
|
|
9
|
+
*
|
|
10
|
+
* ── A visitor is told ONE thing ────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* `useFavorites` disables the query without a mandate, so a visitor's `rows`
|
|
13
|
+
* sit in `loading` forever — and the pane rendered the blocked notice AND the
|
|
14
|
+
* spinner underneath it, saying "you cannot see this" and "we are fetching it"
|
|
15
|
+
* at the same time, with no sign-in link anywhere on the screen. The gate is
|
|
16
|
+
* now the WHOLE body: one designed state, with the door the container supplies
|
|
17
|
+
* (`signIn`) inside it, and nothing else drawn.
|
|
18
|
+
*
|
|
19
|
+
* The pager obeys the same rule. `Previous`/`Next` were rendered disabled in
|
|
20
|
+
* every state including empty, failed and blocked — two controls that meant
|
|
21
|
+
* nothing, three times over. They render when there is a page to go to.
|
|
22
|
+
*
|
|
23
|
+
* And each one renders only when ITS page exists. The keyset gates name the
|
|
24
|
+
* missing page with the button's own label key, so a blocked `Previous` drew
|
|
25
|
+
* the word twice — once as a dead button, once as its own "reason". A pager
|
|
26
|
+
* does not explain itself: the page indicator says where you are, and a
|
|
27
|
+
* direction with nowhere to go is simply not drawn.
|
|
9
28
|
*/
|
|
10
|
-
import type { ReactElement } from "react";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
29
|
+
import type { ReactElement, ReactNode } from "react";
|
|
30
|
+
import { Button, Flex, Typography } from "antd";
|
|
31
|
+
import {
|
|
32
|
+
EmptyState,
|
|
33
|
+
ErrorAlert,
|
|
34
|
+
SkinTheme,
|
|
35
|
+
} from "@stapel/tokens-antd/skin";
|
|
36
|
+
import { matchList, useT } from "@stapel/core";
|
|
37
|
+
import type { LinkComponent, SignInCtaProp } from "@stapel/core";
|
|
38
|
+
import { spacing } from "@stapel/tokens";
|
|
14
39
|
import { useFavorites } from "../headless/Favorites.js";
|
|
15
40
|
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
16
|
-
import { ErrorAlert } from "./ErrorAlert.js";
|
|
17
41
|
import { ListingCard } from "./ListingCard.js";
|
|
18
42
|
import type { ListingCardOpenProps } from "./ListingCard.js";
|
|
19
|
-
import {
|
|
43
|
+
import { SignInLink } from "./SignInLink.js";
|
|
20
44
|
import type { ThemeModeProp } from "./types.js";
|
|
21
45
|
|
|
46
|
+
/** The narrowest a card may get before the grid drops a column — the same
|
|
47
|
+
* element-relative rule the detail gallery uses. It was `width: 240`, which
|
|
48
|
+
* left two cards floating in a 2560px pane and overflowed a 390px one. */
|
|
49
|
+
export const FAVORITES_CARD_MIN = "15rem";
|
|
50
|
+
|
|
51
|
+
/** How wide the saved grid may get before it stops being a grid and becomes
|
|
52
|
+
* three cards adrift in a 1280px window. A measure, not a breakpoint: the
|
|
53
|
+
* cards keep filling it, and past it the page centres. */
|
|
54
|
+
export const FAVORITES_MEASURE = "72rem";
|
|
55
|
+
|
|
22
56
|
/** How a card in this grid opens — the same one-contract union `<ListingCard>`
|
|
23
57
|
* takes, one level up, so a pane cannot re-introduce the double navigation the
|
|
24
58
|
* card no longer allows. */
|
|
@@ -41,7 +75,21 @@ export type FavoritesPaneOpenProps =
|
|
|
41
75
|
readonly linkComponent?: undefined;
|
|
42
76
|
};
|
|
43
77
|
|
|
44
|
-
|
|
78
|
+
/** The way OUT of an empty favourites list. */
|
|
79
|
+
export interface FavoritesBrowseCtaProp {
|
|
80
|
+
/**
|
|
81
|
+
* What a person with nothing saved can do next — a "Browse listings"
|
|
82
|
+
* button, supplied by the container because only it knows where the
|
|
83
|
+
* catalogue lives. An empty state without one is a dead end, and this
|
|
84
|
+
* screen was one of four sharing a single stock illustration and no exit.
|
|
85
|
+
*/
|
|
86
|
+
readonly browseCta?: ReactNode;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type FavoritesPaneProps = ThemeModeProp &
|
|
90
|
+
SignInCtaProp &
|
|
91
|
+
FavoritesBrowseCtaProp &
|
|
92
|
+
FavoritesPaneOpenProps;
|
|
45
93
|
|
|
46
94
|
/** The card's own open props for one row. One arm, never two. */
|
|
47
95
|
function cardOpenProps(
|
|
@@ -60,93 +108,119 @@ function cardOpenProps(
|
|
|
60
108
|
|
|
61
109
|
export function FavoritesPane(props: FavoritesPaneProps): ReactElement {
|
|
62
110
|
const t = useT();
|
|
63
|
-
const describe = useDescribeFlowError();
|
|
64
111
|
const bag = useFavorites();
|
|
112
|
+
const paged = bag.prevPage.available || bag.nextPage.available;
|
|
65
113
|
|
|
66
114
|
return (
|
|
67
|
-
<
|
|
68
|
-
|
|
115
|
+
<SkinTheme
|
|
116
|
+
surface="base"
|
|
117
|
+
style={{ padding: spacing[4], maxWidth: FAVORITES_MEASURE }}
|
|
118
|
+
{...(props.mode !== undefined ? { mode: props.mode } : {})}
|
|
119
|
+
>
|
|
120
|
+
<Flex vertical gap={spacing[4]} data-testid="listings-favorites">
|
|
69
121
|
<Typography.Title level={3}>
|
|
70
122
|
{t(LISTINGS_I18N_KEYS.favoritesTitle)}
|
|
71
123
|
</Typography.Title>
|
|
72
124
|
|
|
73
125
|
{!bag.gate.available ? (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
126
|
+
// The whole body, not a banner over a spinner. A blocked screen has
|
|
127
|
+
// one state and it carries its own way out.
|
|
128
|
+
<EmptyState
|
|
129
|
+
testId="listings-favorites-blocked"
|
|
130
|
+
title={t(bag.gate.block.code, bag.gate.block.params)}
|
|
131
|
+
hint={t(LISTINGS_I18N_KEYS.favoritesSignInHint)}
|
|
132
|
+
action={
|
|
133
|
+
<SignInLink cta={props.signIn} testId="listings-favorites-sign-in" />
|
|
134
|
+
}
|
|
79
135
|
/>
|
|
80
|
-
) :
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
136
|
+
) : (
|
|
137
|
+
<>
|
|
138
|
+
{matchList(bag.rows, {
|
|
139
|
+
loading: () => (
|
|
140
|
+
<div
|
|
141
|
+
role="status"
|
|
142
|
+
aria-busy="true"
|
|
143
|
+
aria-label={t(LISTINGS_I18N_KEYS.favoritesLoading)}
|
|
144
|
+
data-testid="listings-favorites-loading"
|
|
145
|
+
data-stapel-load-state="loading"
|
|
146
|
+
/>
|
|
147
|
+
),
|
|
148
|
+
failed: (error) => (
|
|
149
|
+
<ErrorAlert
|
|
150
|
+
testId="listings-favorites-error"
|
|
151
|
+
thrown={error}
|
|
152
|
+
message={t(LISTINGS_I18N_KEYS.favoritesLoadFailed)}
|
|
153
|
+
onRetry={bag.refetch}
|
|
154
|
+
retryLabel={t(LISTINGS_I18N_KEYS.mineRetry)}
|
|
155
|
+
/>
|
|
156
|
+
),
|
|
157
|
+
empty: () => (
|
|
158
|
+
<EmptyState
|
|
159
|
+
testId="listings-favorites-empty"
|
|
160
|
+
title={t(LISTINGS_I18N_KEYS.favoritesEmpty)}
|
|
161
|
+
hint={t(LISTINGS_I18N_KEYS.favoritesEmptyHint)}
|
|
162
|
+
{...(props.browseCta !== undefined
|
|
163
|
+
? { action: props.browseCta }
|
|
164
|
+
: {})}
|
|
165
|
+
/>
|
|
166
|
+
),
|
|
167
|
+
ready: (rows) => (
|
|
168
|
+
<div
|
|
169
|
+
data-testid="listings-favorites-grid"
|
|
170
|
+
style={{
|
|
171
|
+
display: "grid",
|
|
172
|
+
gridTemplateColumns: `repeat(auto-fill, minmax(${FAVORITES_CARD_MIN}, 1fr))`,
|
|
173
|
+
gap: spacing[4],
|
|
174
|
+
}}
|
|
104
175
|
>
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
/>
|
|
115
|
-
),
|
|
116
|
-
ready: (rows) => (
|
|
117
|
-
<Flex wrap gap={16} data-testid="listings-favorites-grid">
|
|
118
|
-
{rows.map((row) => (
|
|
119
|
-
<div key={row.id} style={{ width: 240 }}>
|
|
120
|
-
<ListingCard listing={row} {...cardOpenProps(props, row.id)} />
|
|
176
|
+
{rows.map((row) => (
|
|
177
|
+
<ListingCard
|
|
178
|
+
key={row.id}
|
|
179
|
+
listing={row}
|
|
180
|
+
blockedReason="line"
|
|
181
|
+
{...(props.signIn !== undefined ? { signIn: props.signIn } : {})}
|
|
182
|
+
{...cardOpenProps(props, row.id)}
|
|
183
|
+
/>
|
|
184
|
+
))}
|
|
121
185
|
</div>
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
),
|
|
125
|
-
})}
|
|
186
|
+
),
|
|
187
|
+
})}
|
|
126
188
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
189
|
+
{paged ? (
|
|
190
|
+
<Flex gap={spacing[3]} wrap align="center">
|
|
191
|
+
{bag.prevPage.available ? (
|
|
192
|
+
<Button
|
|
193
|
+
size="small"
|
|
194
|
+
data-testid="listings-favorites-prev"
|
|
195
|
+
data-analytics="none"
|
|
196
|
+
data-analytics-reason="paging a list the person is already reading; not a business action"
|
|
197
|
+
onClick={bag.goPrev}
|
|
198
|
+
>
|
|
199
|
+
{t(LISTINGS_I18N_KEYS.pagePrev)}
|
|
200
|
+
</Button>
|
|
201
|
+
) : null}
|
|
202
|
+
<Typography.Text
|
|
203
|
+
type="secondary"
|
|
204
|
+
data-testid="listings-favorites-page"
|
|
205
|
+
>
|
|
206
|
+
{t(LISTINGS_I18N_KEYS.pageIndicator, { page: bag.pageNumber })}
|
|
207
|
+
</Typography.Text>
|
|
208
|
+
{bag.nextPage.available ? (
|
|
209
|
+
<Button
|
|
210
|
+
size="small"
|
|
211
|
+
data-testid="listings-favorites-next"
|
|
212
|
+
data-analytics="none"
|
|
213
|
+
data-analytics-reason="paging a list the person is already reading; not a business action"
|
|
214
|
+
onClick={bag.goNext}
|
|
215
|
+
>
|
|
216
|
+
{t(LISTINGS_I18N_KEYS.pageNext)}
|
|
217
|
+
</Button>
|
|
218
|
+
) : null}
|
|
219
|
+
</Flex>
|
|
220
|
+
) : null}
|
|
221
|
+
</>
|
|
222
|
+
)}
|
|
149
223
|
</Flex>
|
|
150
|
-
</
|
|
224
|
+
</SkinTheme>
|
|
151
225
|
);
|
|
152
226
|
}
|