@stapel/listings-react 0.1.0 → 0.3.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 +194 -0
- package/LICENSE +21 -0
- package/MODULE.md +97 -3
- package/README.md +67 -2
- package/dist/api/types.d.ts +1 -1
- package/dist/api/types.js +1 -1
- package/dist/default/FavoritesPane.d.ts +19 -4
- package/dist/default/FavoritesPane.d.ts.map +1 -1
- package/dist/default/FavoritesPane.js +13 -3
- package/dist/default/FavoritesPane.js.map +1 -1
- package/dist/default/ListingCard.d.ts +54 -11
- package/dist/default/ListingCard.d.ts.map +1 -1
- package/dist/default/ListingCard.js +32 -5
- package/dist/default/ListingCard.js.map +1 -1
- package/dist/default/ListingComposerPage.d.ts +59 -4
- package/dist/default/ListingComposerPage.d.ts.map +1 -1
- package/dist/default/ListingComposerPage.js +12 -3
- package/dist/default/ListingComposerPage.js.map +1 -1
- package/dist/default/SignInLink.d.ts +28 -0
- package/dist/default/SignInLink.d.ts.map +1 -0
- package/dist/default/SignInLink.js +17 -0
- package/dist/default/SignInLink.js.map +1 -0
- package/dist/default/index.d.ts +5 -3
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +1 -0
- package/dist/default/index.js.map +1 -1
- package/dist/headless/ListingComposer.d.ts +17 -0
- package/dist/headless/ListingComposer.d.ts.map +1 -1
- package/dist/headless/ListingComposer.js +26 -10
- package/dist/headless/ListingComposer.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +1 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/keys.d.ts +2 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +3 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +2 -1
- package/dist/i18n/ru.js.map +1 -1
- package/llms.txt +1 -1
- package/manifest.json +2 -1
- package/nav-manifest.json +1 -1
- package/package.json +16 -16
- package/src/analytics/generated/events.json +1 -1
- package/src/default/FavoritesPane.tsx +40 -11
- package/src/default/ListingCard.tsx +144 -27
- package/src/default/ListingComposerPage.tsx +79 -14
- package/src/default/SignInLink.tsx +54 -0
- package/src/default/index.ts +15 -3
- package/src/headless/ListingComposer.tsx +43 -10
- package/src/i18n/es.ts +1 -0
- package/src/i18n/keys.ts +3 -0
- package/src/i18n/ru.ts +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The door beside a blocked control.
|
|
3
|
+
*
|
|
4
|
+
* `actionBlocked` ended the grey-rectangle incident by making every
|
|
5
|
+
* switched-off control state its reason. It did not end the next problem:
|
|
6
|
+
* "sign in to add this to favourites" is a reason whose next action is a LINK,
|
|
7
|
+
* and this pair rendered the sentence and stopped there — leaving the visitor
|
|
8
|
+
* to find the header themselves (storefront Wave D, G-3).
|
|
9
|
+
*
|
|
10
|
+
* WHERE that link goes is the container's business, never the pair's: the
|
|
11
|
+
* storefront's is `/login?next=<current>`, a tenant app's may be a modal. So
|
|
12
|
+
* the shape is core's `SignInCta` — `{href}` or `{onSignIn}`, never both — and
|
|
13
|
+
* the copy is this pair's, because core floors `en` and `ru` while this pair
|
|
14
|
+
* also ships `es`.
|
|
15
|
+
*
|
|
16
|
+
* A host that routes internally passes `onSignIn`; the `href` arm renders a
|
|
17
|
+
* plain anchor on purpose, because arriving at a sign-in page is one of the
|
|
18
|
+
* few navigations a full load costs nothing.
|
|
19
|
+
*/
|
|
20
|
+
import type { ReactElement } from "react";
|
|
21
|
+
import { Typography } from "antd";
|
|
22
|
+
import { useT } from "@stapel/core";
|
|
23
|
+
import type { SignInCta } from "@stapel/core";
|
|
24
|
+
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
25
|
+
|
|
26
|
+
export interface SignInLinkProps {
|
|
27
|
+
/** Absent: no link — a host with no sign-in route shows the reason alone. */
|
|
28
|
+
readonly cta: SignInCta | undefined;
|
|
29
|
+
readonly testId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function SignInLink(props: SignInLinkProps): ReactElement | null {
|
|
33
|
+
const t = useT();
|
|
34
|
+
const { cta } = props;
|
|
35
|
+
if (cta === undefined) return null;
|
|
36
|
+
// The separating space belongs HERE, not at the call site: a reason with no
|
|
37
|
+
// door must render as exactly its own sentence, and a `{" "}` left behind by
|
|
38
|
+
// an absent link is a trailing space in every caller's assertion.
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{" "}
|
|
42
|
+
<Typography.Link
|
|
43
|
+
data-testid={props.testId}
|
|
44
|
+
data-analytics="none"
|
|
45
|
+
data-analytics-reason="business action — host app wraps with its own tracked()"
|
|
46
|
+
{...(cta.href !== undefined
|
|
47
|
+
? { href: cta.href }
|
|
48
|
+
: { onClick: cta.onSignIn })}
|
|
49
|
+
>
|
|
50
|
+
{t(LISTINGS_I18N_KEYS.cardSignIn)}
|
|
51
|
+
</Typography.Link>
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/default/index.ts
CHANGED
|
@@ -15,21 +15,33 @@
|
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
17
|
export { ListingCard } from "./ListingCard.js";
|
|
18
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
ListingCardProps,
|
|
20
|
+
ListingCardBaseProps,
|
|
21
|
+
ListingCardOpenProps,
|
|
22
|
+
} from "./ListingCard.js";
|
|
19
23
|
export { ListingDetailPane } from "./ListingDetailPane.js";
|
|
20
24
|
export type { ListingDetailPaneProps } from "./ListingDetailPane.js";
|
|
21
25
|
export { ListingComposerPage } from "./ListingComposerPage.js";
|
|
22
|
-
export type {
|
|
26
|
+
export type {
|
|
27
|
+
ListingComposerPageProps,
|
|
28
|
+
ComposerCategorySlot,
|
|
29
|
+
} from "./ListingComposerPage.js";
|
|
23
30
|
export { MyListingsPane } from "./MyListingsPane.js";
|
|
24
31
|
export type { MyListingsPaneProps } from "./MyListingsPane.js";
|
|
25
32
|
export { FavoritesPane } from "./FavoritesPane.js";
|
|
26
|
-
export type {
|
|
33
|
+
export type {
|
|
34
|
+
FavoritesPaneProps,
|
|
35
|
+
FavoritesPaneOpenProps,
|
|
36
|
+
} from "./FavoritesPane.js";
|
|
27
37
|
|
|
28
38
|
export { LifecycleTag, ListingStatusBlock, ModerationNote } from "./StatusTags.js";
|
|
29
39
|
export type { ListingStatusProps } from "./StatusTags.js";
|
|
30
40
|
export { ListingPhoto } from "./ListingPhoto.js";
|
|
31
41
|
export type { ListingPhotoProps } from "./ListingPhoto.js";
|
|
32
42
|
export { ErrorAlert } from "./ErrorAlert.js";
|
|
43
|
+
export { SignInLink } from "./SignInLink.js";
|
|
44
|
+
export type { SignInLinkProps } from "./SignInLink.js";
|
|
33
45
|
export { ListingsSkinTheme } from "./theme.js";
|
|
34
46
|
export type { ListingsSkinThemeProps } from "./theme.js";
|
|
35
47
|
export type { ThemeModeProp } from "./types.js";
|
|
@@ -125,6 +125,23 @@ export interface UseListingComposerOptions {
|
|
|
125
125
|
/** The upload queue. Absent: no gallery, and `images` is set through
|
|
126
126
|
* `setValue` by whatever the host uses instead. */
|
|
127
127
|
readonly images?: ListingImagesBag;
|
|
128
|
+
/**
|
|
129
|
+
* The chosen category, when the CONTAINER owns that state.
|
|
130
|
+
*
|
|
131
|
+
* The category is the one value the composer cannot own alone: the picker
|
|
132
|
+
* lives in `@stapel/categories-react` and the schema read
|
|
133
|
+
* (`useCategoryFeatures(id)`) that fills `features` is keyed by it, so the
|
|
134
|
+
* container holds it either way. Passing it here makes this hook controlled
|
|
135
|
+
* on that field — `values.categoryId` mirrors it, and `setCategory` reports
|
|
136
|
+
* upwards instead of writing local state that would then disagree with the
|
|
137
|
+
* schema on screen.
|
|
138
|
+
*
|
|
139
|
+
* Absent: uncontrolled, exactly as before.
|
|
140
|
+
*/
|
|
141
|
+
readonly category?: string;
|
|
142
|
+
/** Called by `setCategory`, whether or not `category` is controlled. This is
|
|
143
|
+
* how a container learns which category to read the schema for. */
|
|
144
|
+
readonly onCategoryChange?: (categoryId: string) => void;
|
|
128
145
|
/** Seed for a brand-new draft (a category preselected from the URL, say). */
|
|
129
146
|
readonly initialValues?: Partial<ListingDraftValues>;
|
|
130
147
|
onDraftCreated?: (draft: ListingDraft) => void;
|
|
@@ -245,9 +262,19 @@ export function useListingComposer(
|
|
|
245
262
|
// The gallery is the upload bag's, whenever there is one: two sources of
|
|
246
263
|
// truth for the same list is how a publish sends photos the person removed.
|
|
247
264
|
const images = options.images?.refs ?? values.images;
|
|
265
|
+
// Same rule for the category when the container holds it: the schema on
|
|
266
|
+
// screen is read for the container's id, so a local copy that drifted from
|
|
267
|
+
// it would ask one category's questions and file the listing under another.
|
|
268
|
+
const controlledCategory = options.category;
|
|
248
269
|
const effectiveValues: ListingDraftValues = useMemo(
|
|
249
|
-
() => ({
|
|
250
|
-
|
|
270
|
+
() => ({
|
|
271
|
+
...values,
|
|
272
|
+
images,
|
|
273
|
+
...(controlledCategory !== undefined
|
|
274
|
+
? { categoryId: controlledCategory }
|
|
275
|
+
: {}),
|
|
276
|
+
}),
|
|
277
|
+
[values, images, controlledCategory]
|
|
251
278
|
);
|
|
252
279
|
|
|
253
280
|
const featuresDto = useMemo(
|
|
@@ -435,14 +462,20 @@ export function useListingComposer(
|
|
|
435
462
|
},
|
|
436
463
|
setCategory: (categoryId) => {
|
|
437
464
|
setSaved(false);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
465
|
+
// Told upwards FIRST and unconditionally: the container's schema read is
|
|
466
|
+
// keyed by this id, and it must be asked for even when the composer is
|
|
467
|
+
// uncontrolled — that is the wire `features` arrives on.
|
|
468
|
+
options.onCategoryChange?.(categoryId);
|
|
469
|
+
if (controlledCategory === undefined) {
|
|
470
|
+
setValues((current) => {
|
|
471
|
+
if (current.categoryId === categoryId) return current;
|
|
472
|
+
// The features of the NEW category are not known yet (the
|
|
473
|
+
// container's schema read has not run), so nothing is pruned here —
|
|
474
|
+
// pruning happens in the effect below, once they arrive. Recording
|
|
475
|
+
// the intent, not guessing the outcome.
|
|
476
|
+
return { ...current, categoryId };
|
|
477
|
+
});
|
|
478
|
+
}
|
|
446
479
|
setDropped([]);
|
|
447
480
|
setRefusal(undefined);
|
|
448
481
|
},
|
package/src/i18n/es.ts
CHANGED
|
@@ -70,6 +70,7 @@ export const listingsI18nBundleEs: I18nDictionary = {
|
|
|
70
70
|
"listings.card.favorite_add": "Guardar en favoritos",
|
|
71
71
|
"listings.card.favorite_remove": "Quitar de favoritos",
|
|
72
72
|
"listings.card.open": "Abrir",
|
|
73
|
+
"listings.card.sign_in": "Iniciar sesión",
|
|
73
74
|
|
|
74
75
|
"listings.detail.loading": "Cargando el anuncio…",
|
|
75
76
|
"listings.detail.load_failed": "No pudimos cargar este anuncio",
|
package/src/i18n/keys.ts
CHANGED
|
@@ -63,6 +63,8 @@ export const LISTINGS_I18N_KEYS = {
|
|
|
63
63
|
cardFavoriteAdd: "listings.card.favorite_add",
|
|
64
64
|
cardFavoriteRemove: "listings.card.favorite_remove",
|
|
65
65
|
cardOpen: "listings.card.open",
|
|
66
|
+
/** The door beside a blocked favourite: the container supplies WHERE. */
|
|
67
|
+
cardSignIn: "listings.card.sign_in",
|
|
66
68
|
|
|
67
69
|
// ── detail ───────────────────────────────────────────────────────────────
|
|
68
70
|
detailLoading: "listings.detail.loading",
|
|
@@ -218,6 +220,7 @@ export const listingsI18nBundleEn: Record<string, string> = {
|
|
|
218
220
|
"listings.card.favorite_add": "Save to favourites",
|
|
219
221
|
"listings.card.favorite_remove": "Remove from favourites",
|
|
220
222
|
"listings.card.open": "Open",
|
|
223
|
+
"listings.card.sign_in": "Sign in",
|
|
221
224
|
|
|
222
225
|
"listings.detail.loading": "Loading the listing…",
|
|
223
226
|
"listings.detail.load_failed": "We could not load this listing",
|
package/src/i18n/ru.ts
CHANGED
|
@@ -86,6 +86,7 @@ export const listingsI18nBundleRu: I18nDictionary = {
|
|
|
86
86
|
"listings.card.favorite_add": "В избранное",
|
|
87
87
|
"listings.card.favorite_remove": "Убрать из избранного",
|
|
88
88
|
"listings.card.open": "Открыть",
|
|
89
|
+
"listings.card.sign_in": "Войти",
|
|
89
90
|
|
|
90
91
|
"listings.detail.loading": "Загружаем объявление…",
|
|
91
92
|
"listings.detail.load_failed": "Не удалось загрузить объявление",
|