@stapel/listings-react 0.1.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 +1 -0
- package/MODULE.md +191 -0
- package/README.md +169 -0
- package/dist/api/generated/schema.d.ts +1253 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/listingsApi.d.ts +145 -0
- package/dist/api/listingsApi.d.ts.map +1 -0
- package/dist/api/listingsApi.js +38 -0
- package/dist/api/listingsApi.js.map +1 -0
- package/dist/api/types.d.ts +156 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +37 -0
- package/dist/api/types.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +9 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +27 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/FavoritesPane.d.ts +18 -0
- package/dist/default/FavoritesPane.d.ts.map +1 -0
- package/dist/default/FavoritesPane.js +28 -0
- package/dist/default/FavoritesPane.js.map +1 -0
- package/dist/default/ListingCard.d.ts +42 -0
- package/dist/default/ListingCard.d.ts.map +1 -0
- package/dist/default/ListingCard.js +37 -0
- package/dist/default/ListingCard.js.map +1 -0
- package/dist/default/ListingComposerPage.d.ts +46 -0
- package/dist/default/ListingComposerPage.d.ts.map +1 -0
- package/dist/default/ListingComposerPage.js +99 -0
- package/dist/default/ListingComposerPage.js.map +1 -0
- package/dist/default/ListingDetailPane.d.ts +33 -0
- package/dist/default/ListingDetailPane.d.ts.map +1 -0
- package/dist/default/ListingDetailPane.js +48 -0
- package/dist/default/ListingDetailPane.js.map +1 -0
- package/dist/default/ListingPhoto.d.ts +25 -0
- package/dist/default/ListingPhoto.d.ts.map +1 -0
- package/dist/default/ListingPhoto.js +20 -0
- package/dist/default/ListingPhoto.js.map +1 -0
- package/dist/default/MyListingsPane.d.ts +25 -0
- package/dist/default/MyListingsPane.d.ts.map +1 -0
- package/dist/default/MyListingsPane.js +84 -0
- package/dist/default/MyListingsPane.js.map +1 -0
- package/dist/default/StatusTags.d.ts +31 -0
- package/dist/default/StatusTags.d.ts.map +1 -0
- package/dist/default/StatusTags.js +40 -0
- package/dist/default/StatusTags.js.map +1 -0
- package/dist/default/icons.d.ts +16 -0
- package/dist/default/icons.d.ts.map +1 -0
- package/dist/default/icons.js +5 -0
- package/dist/default/icons.js.map +1 -0
- package/dist/default/index.d.ts +35 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +26 -0
- package/dist/default/index.js.map +1 -0
- package/dist/default/theme.d.ts +25 -0
- package/dist/default/theme.d.ts.map +1 -0
- package/dist/default/theme.js +40 -0
- package/dist/default/theme.js.map +1 -0
- package/dist/default/types.d.ts +12 -0
- package/dist/default/types.d.ts.map +1 -0
- package/dist/default/types.js +2 -0
- package/dist/default/types.js.map +1 -0
- package/dist/flows/registry.d.ts +35 -0
- package/dist/flows/registry.d.ts.map +1 -0
- package/dist/flows/registry.js +33 -0
- package/dist/flows/registry.js.map +1 -0
- package/dist/headless/Favorites.d.ts +52 -0
- package/dist/headless/Favorites.d.ts.map +1 -0
- package/dist/headless/Favorites.js +78 -0
- package/dist/headless/Favorites.js.map +1 -0
- package/dist/headless/ListingActions.d.ts +30 -0
- package/dist/headless/ListingActions.d.ts.map +1 -0
- package/dist/headless/ListingActions.js +54 -0
- package/dist/headless/ListingActions.js.map +1 -0
- package/dist/headless/ListingComposer.d.ts +133 -0
- package/dist/headless/ListingComposer.d.ts.map +1 -0
- package/dist/headless/ListingComposer.js +246 -0
- package/dist/headless/ListingComposer.js.map +1 -0
- package/dist/headless/ListingDetail.d.ts +81 -0
- package/dist/headless/ListingDetail.d.ts.map +1 -0
- package/dist/headless/ListingDetail.js +101 -0
- package/dist/headless/ListingDetail.js.map +1 -0
- package/dist/headless/ListingsProvider.d.ts +17 -0
- package/dist/headless/ListingsProvider.d.ts.map +1 -0
- package/dist/headless/ListingsProvider.js +16 -0
- package/dist/headless/ListingsProvider.js.map +1 -0
- package/dist/headless/MyListings.d.ts +44 -0
- package/dist/headless/MyListings.d.ts.map +1 -0
- package/dist/headless/MyListings.js +82 -0
- package/dist/headless/MyListings.js.map +1 -0
- package/dist/headless/useMandateGate.d.ts +28 -0
- package/dist/headless/useMandateGate.d.ts.map +1 -0
- package/dist/headless/useMandateGate.js +39 -0
- package/dist/headless/useMandateGate.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +34 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +45 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +21 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +146 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +58 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +413 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +210 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +58 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +170 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +295 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +37 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +162 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +98 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/draft.d.ts +138 -0
- package/dist/model/draft.d.ts.map +1 -0
- package/dist/model/draft.js +192 -0
- package/dist/model/draft.js.map +1 -0
- package/dist/model/features.d.ts +69 -0
- package/dist/model/features.d.ts.map +1 -0
- package/dist/model/features.js +73 -0
- package/dist/model/features.js.map +1 -0
- package/dist/model/mineSource.d.ts +47 -0
- package/dist/model/mineSource.d.ts.map +1 -0
- package/dist/model/mineSource.js +45 -0
- package/dist/model/mineSource.js.map +1 -0
- package/dist/model/mutations.d.ts +60 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +165 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +75 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +117 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +50 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +24 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/runtime.d.ts +69 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +14 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/model/status.d.ts +117 -0
- package/dist/model/status.d.ts.map +1 -0
- package/dist/model/status.js +164 -0
- package/dist/model/status.js.map +1 -0
- package/dist/model/transitions.d.ts +41 -0
- package/dist/model/transitions.d.ts.map +1 -0
- package/dist/model/transitions.js +25 -0
- package/dist/model/transitions.js.map +1 -0
- package/dist/model/validation.d.ts +135 -0
- package/dist/model/validation.d.ts.map +1 -0
- package/dist/model/validation.js +191 -0
- package/dist/model/validation.js.map +1 -0
- package/dist/nav/manifest.d.ts +46 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +56 -0
- package/dist/nav/manifest.js.map +1 -0
- package/llms.txt +118 -0
- package/manifest.json +1044 -0
- package/nav-manifest.json +84 -0
- package/package.json +121 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/generated/schema.ts +1255 -0
- package/src/api/listingsApi.ts +237 -0
- package/src/api/types.ts +193 -0
- package/src/default/ErrorAlert.tsx +46 -0
- package/src/default/FavoritesPane.tsx +123 -0
- package/src/default/ListingCard.tsx +178 -0
- package/src/default/ListingComposerPage.tsx +466 -0
- package/src/default/ListingDetailPane.tsx +271 -0
- package/src/default/ListingPhoto.tsx +62 -0
- package/src/default/MyListingsPane.tsx +333 -0
- package/src/default/StatusTags.tsx +86 -0
- package/src/default/icons.tsx +31 -0
- package/src/default/index.ts +35 -0
- package/src/default/theme.tsx +67 -0
- package/src/default/types.ts +12 -0
- package/src/flows/registry.ts +45 -0
- package/src/headless/Favorites.tsx +137 -0
- package/src/headless/ListingActions.tsx +101 -0
- package/src/headless/ListingComposer.tsx +476 -0
- package/src/headless/ListingDetail.tsx +231 -0
- package/src/headless/ListingsProvider.tsx +20 -0
- package/src/headless/MyListings.tsx +148 -0
- package/src/headless/useMandateGate.ts +40 -0
- package/src/i18n/errorsMap.ts +62 -0
- package/src/i18n/es.ts +198 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +241 -0
- package/src/i18n/generated/errors.json +499 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +356 -0
- package/src/i18n/ru.ts +215 -0
- package/src/index.ts +260 -0
- package/src/model/context.tsx +28 -0
- package/src/model/draft.ts +307 -0
- package/src/model/features.ts +122 -0
- package/src/model/mineSource.ts +55 -0
- package/src/model/mutations.ts +224 -0
- package/src/model/queries.ts +150 -0
- package/src/model/queryKeys.ts +76 -0
- package/src/model/runtime.ts +87 -0
- package/src/model/status.ts +279 -0
- package/src/model/transitions.ts +62 -0
- package/src/model/validation.ts +271 -0
- package/src/nav/manifest.ts +97 -0
- package/tsconfig.json +26 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@stapel/listings-react` — the headless React pair for stapel-listings
|
|
3
|
+
* (frontend-standard §2). Business + state only, zero visual opinion; the
|
|
4
|
+
* antd skin lives behind the `./default` subpath, so a host that renders its
|
|
5
|
+
* own shop window carries none of it.
|
|
6
|
+
*
|
|
7
|
+
* ── The one-liner ──────────────────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const runtime = createListingsRuntime({
|
|
11
|
+
* baseUrl: "/listings/api/v1/",
|
|
12
|
+
* resolveImage: (ref) => myCdn.describe(ref),
|
|
13
|
+
* });
|
|
14
|
+
* <ListingsProvider runtime={runtime}>
|
|
15
|
+
* <ListingDetailPane id={id} />
|
|
16
|
+
* </ListingsProvider>
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* ── The four properties this pair exists to guarantee ──────────────────────
|
|
20
|
+
*
|
|
21
|
+
* 1. **Two axes, both on screen, neither standing in for the other.**
|
|
22
|
+
* `status` decides visibility and nothing else does; `moderation_status`
|
|
23
|
+
* decides nothing about it. Since stapel-listings 0.5.0 they genuinely
|
|
24
|
+
* diverge — an edit to a LIVE listing keeps `status: published` and moves
|
|
25
|
+
* only the moderation axis — so "published, changes under review" is a
|
|
26
|
+
* sentence this pair can say, and a dashboard that computed either field
|
|
27
|
+
* from the other could not. `model/status.ts` produces both halves from
|
|
28
|
+
* both fields, once, and the 9 × 4 table is asserted.
|
|
29
|
+
* 2. **A refusal lands on the control that caused it.** `publish` answers an
|
|
30
|
+
* invalid draft with a BARE `ValidationBatchResult` (not an error
|
|
31
|
+
* envelope), and the engine's rows carry `{feature, slug}` and never
|
|
32
|
+
* `field`. `model/validation.ts` splits the two kinds of 400 apart and
|
|
33
|
+
* `featureErrorsBySlug` adds the routing key, so "this box is wrong" never
|
|
34
|
+
* degrades into "something is wrong".
|
|
35
|
+
* 3. **Every switched-off control states its reason.** Signing in, waiting
|
|
36
|
+
* for photos, a type this build cannot draw, a lifecycle move the server
|
|
37
|
+
* would refuse — each is an `ActionAvailability` with a named block, never
|
|
38
|
+
* a grey button and never a hidden one.
|
|
39
|
+
* 4. **An absence is never rendered as a zero.** A soft-deleted listing says
|
|
40
|
+
* it was removed rather than 404-ing like a typo; a stored feature this
|
|
41
|
+
* build cannot read is counted rather than dropped; and the owner's own
|
|
42
|
+
* listings — which stapel-listings 0.6.1 has NO endpoint for — arrive as a
|
|
43
|
+
* named failure rather than an empty grid (`headless/MyListings.tsx`).
|
|
44
|
+
*
|
|
45
|
+
* ── The seams, and why they are not imports ────────────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* `@stapel/cdn-react` and `@stapel/categories-react` are L2 pairs, and L2
|
|
48
|
+
* pairs never import each other. The gallery therefore arrives as a
|
|
49
|
+
* structural {@link ListingImagesBag} (satisfied by cdn's upload bag: its
|
|
50
|
+
* `refs` IS `images_draft` and its `settled` is the submit gate), the
|
|
51
|
+
* category schema arrives as a plain `FeatureDef[]`, and a stored image
|
|
52
|
+
* reference is resolved by a host-supplied {@link ListingImageResolver} —
|
|
53
|
+
* because no contract in this fleet resolves a stranger's CDN reference, and
|
|
54
|
+
* a pair that invented a URL convention would be writing a contract nobody
|
|
55
|
+
* agreed to. `@stapel/attributes-react` IS a dependency; it is L0.
|
|
56
|
+
*
|
|
57
|
+
* Layers: api → model → flows → headless → i18n. Generated surfaces (the
|
|
58
|
+
* typed schema, the error map, the manifest, the nav manifest, llms.txt) are
|
|
59
|
+
* produced by the monorepo `gen:*` drivers from stapel-listings' own `docs/`
|
|
60
|
+
* artifacts and stand under drift gates.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
// ── api ──────────────────────────────────────────────────────────────────────
|
|
64
|
+
export { createListingsApi } from "./api/listingsApi.js";
|
|
65
|
+
export type { ListingsApi } from "./api/listingsApi.js";
|
|
66
|
+
export {
|
|
67
|
+
DEFAULT_LISTING_CURRENCY,
|
|
68
|
+
LISTING_STATUSES,
|
|
69
|
+
MODERATION_STATUSES,
|
|
70
|
+
} from "./api/types.js";
|
|
71
|
+
export type {
|
|
72
|
+
DeleteResponse,
|
|
73
|
+
FavoriteToggleResponse,
|
|
74
|
+
FeatureConfig,
|
|
75
|
+
FeatureDef,
|
|
76
|
+
FeatureValueDto,
|
|
77
|
+
ListingActionResponse,
|
|
78
|
+
ListingCard,
|
|
79
|
+
ListingDetail as ListingDetailData,
|
|
80
|
+
ListingDraft,
|
|
81
|
+
ListingDraftPatch,
|
|
82
|
+
ListingFeatureDao,
|
|
83
|
+
ListingFeatureDaoUnion,
|
|
84
|
+
ListingFeatureType,
|
|
85
|
+
ListingFeatureView,
|
|
86
|
+
ListingLifecycleStatus,
|
|
87
|
+
ListingModerationStatus,
|
|
88
|
+
ListingPageParams,
|
|
89
|
+
ListingStatusInfo,
|
|
90
|
+
MyCounters,
|
|
91
|
+
PaginatedListingCards,
|
|
92
|
+
PublishResponse,
|
|
93
|
+
Schemas,
|
|
94
|
+
} from "./api/types.js";
|
|
95
|
+
|
|
96
|
+
// ── model: the two axes ──────────────────────────────────────────────────────
|
|
97
|
+
export {
|
|
98
|
+
MY_LISTINGS_TABS,
|
|
99
|
+
MY_LISTINGS_TAB_STATUSES,
|
|
100
|
+
isPubliclyVisible,
|
|
101
|
+
lifecycleCaption,
|
|
102
|
+
listingStatusView,
|
|
103
|
+
moderationNotice,
|
|
104
|
+
tabOf,
|
|
105
|
+
} from "./model/status.js";
|
|
106
|
+
export type {
|
|
107
|
+
LifecycleCaption,
|
|
108
|
+
ListingStatusTone,
|
|
109
|
+
ListingStatusView,
|
|
110
|
+
ModerationNotice,
|
|
111
|
+
MyListingsTab,
|
|
112
|
+
} from "./model/status.js";
|
|
113
|
+
export { LISTING_TRANSITIONS, canDelete, canTransition } from "./model/transitions.js";
|
|
114
|
+
|
|
115
|
+
// ── model: the owner-list gap, named ─────────────────────────────────────────
|
|
116
|
+
export { MY_LISTINGS_SOURCE_MISSING } from "./model/mineSource.js";
|
|
117
|
+
export type { MyListingsSource } from "./model/mineSource.js";
|
|
118
|
+
|
|
119
|
+
// ── model: the draft twin ────────────────────────────────────────────────────
|
|
120
|
+
export {
|
|
121
|
+
EMPTY_LOCATION,
|
|
122
|
+
createDraftBody,
|
|
123
|
+
draftPatchFromValues,
|
|
124
|
+
draftValuesFromDetail,
|
|
125
|
+
draftValuesFromWire,
|
|
126
|
+
droppedFeatureSlugs,
|
|
127
|
+
emptyDraftValues,
|
|
128
|
+
retainKnownFeatureValues,
|
|
129
|
+
} from "./model/draft.js";
|
|
130
|
+
export type {
|
|
131
|
+
EmptyDraftOptions,
|
|
132
|
+
ListingDraftValues,
|
|
133
|
+
ListingLocation,
|
|
134
|
+
} from "./model/draft.js";
|
|
135
|
+
|
|
136
|
+
// ── model: the stored feature projection ─────────────────────────────────────
|
|
137
|
+
export {
|
|
138
|
+
asFeatureDaoList,
|
|
139
|
+
featureFromDao,
|
|
140
|
+
featuresDtoFromDaoList,
|
|
141
|
+
featuresFromDaoList,
|
|
142
|
+
unreadableFeatureCount,
|
|
143
|
+
} from "./model/features.js";
|
|
144
|
+
|
|
145
|
+
// ── model: validation, the mirror and the publish-400 split ──────────────────
|
|
146
|
+
export {
|
|
147
|
+
CATEGORY_FIELD,
|
|
148
|
+
DEFAULT_DRAFT_LIMITS,
|
|
149
|
+
DESCRIPTION_FIELD,
|
|
150
|
+
IMAGES_FIELD,
|
|
151
|
+
PRICE_FIELD,
|
|
152
|
+
TITLE_FIELD,
|
|
153
|
+
failedResults,
|
|
154
|
+
isBatchValid,
|
|
155
|
+
listingFieldErrors,
|
|
156
|
+
mirrorDraft,
|
|
157
|
+
mirrorListingFields,
|
|
158
|
+
publishRefusal,
|
|
159
|
+
} from "./model/validation.js";
|
|
160
|
+
export type { ListingDraftLimits, PublishRefusal } from "./model/validation.js";
|
|
161
|
+
|
|
162
|
+
// ── model: runtime wiring, query keys, context, hooks ────────────────────────
|
|
163
|
+
export { createListingsRuntime } from "./model/runtime.js";
|
|
164
|
+
export type {
|
|
165
|
+
CreateListingsRuntimeOptions,
|
|
166
|
+
ListingImageResolver,
|
|
167
|
+
ListingsRuntime,
|
|
168
|
+
} from "./model/runtime.js";
|
|
169
|
+
export {
|
|
170
|
+
ListingsRuntimeContext,
|
|
171
|
+
useListingsAnalytics,
|
|
172
|
+
useListingsApi,
|
|
173
|
+
useListingsRuntime,
|
|
174
|
+
} from "./model/context.js";
|
|
175
|
+
export { listingsQueryKeys, pageKey } from "./model/queryKeys.js";
|
|
176
|
+
export type { ListingPageKey } from "./model/queryKeys.js";
|
|
177
|
+
export {
|
|
178
|
+
useListing,
|
|
179
|
+
useListingCards,
|
|
180
|
+
useListingStatus,
|
|
181
|
+
useMyCounters,
|
|
182
|
+
useMyFavorites,
|
|
183
|
+
useValidateDraft,
|
|
184
|
+
} from "./model/queries.js";
|
|
185
|
+
export {
|
|
186
|
+
useArchiveListing,
|
|
187
|
+
useCompleteListing,
|
|
188
|
+
useCreateDraft,
|
|
189
|
+
useDeleteListing,
|
|
190
|
+
useFavoriteListing,
|
|
191
|
+
usePublishListing,
|
|
192
|
+
useSaveDraft,
|
|
193
|
+
} from "./model/mutations.js";
|
|
194
|
+
export type { FavoriteInput, SaveDraftInput } from "./model/mutations.js";
|
|
195
|
+
|
|
196
|
+
// ── flows (zero-flow shim — stapel-listings annotates none) ──────────────────
|
|
197
|
+
export { LISTINGS_FLOWS, flowEndpoints } from "./flows/registry.js";
|
|
198
|
+
export type {
|
|
199
|
+
FlowEndpoint,
|
|
200
|
+
ListingsFlowId,
|
|
201
|
+
ListingsFlowSpec,
|
|
202
|
+
} from "./flows/registry.js";
|
|
203
|
+
|
|
204
|
+
// ── headless (renderless components + their bags) ────────────────────────────
|
|
205
|
+
export { ListingsProvider } from "./headless/ListingsProvider.js";
|
|
206
|
+
export { useMandateGate } from "./headless/useMandateGate.js";
|
|
207
|
+
export { ListingDetail, useListingDetail } from "./headless/ListingDetail.js";
|
|
208
|
+
export type {
|
|
209
|
+
ListingDetailBag,
|
|
210
|
+
UseListingDetailOptions,
|
|
211
|
+
} from "./headless/ListingDetail.js";
|
|
212
|
+
export { ListingComposer, useListingComposer } from "./headless/ListingComposer.js";
|
|
213
|
+
export type {
|
|
214
|
+
ComposeStage,
|
|
215
|
+
ListingComposerBag,
|
|
216
|
+
ListingImagesBag,
|
|
217
|
+
PublishOutcome,
|
|
218
|
+
UseListingComposerOptions,
|
|
219
|
+
} from "./headless/ListingComposer.js";
|
|
220
|
+
export { MyListings, useMyListings } from "./headless/MyListings.js";
|
|
221
|
+
export type {
|
|
222
|
+
MyListingsBag,
|
|
223
|
+
UseMyListingsOptions,
|
|
224
|
+
} from "./headless/MyListings.js";
|
|
225
|
+
export { Favorites, useFavorites, useFavoriteToggle } from "./headless/Favorites.js";
|
|
226
|
+
export type {
|
|
227
|
+
FavoriteToggleBag,
|
|
228
|
+
FavoritesBag,
|
|
229
|
+
UseFavoritesOptions,
|
|
230
|
+
} from "./headless/Favorites.js";
|
|
231
|
+
export { useListingActions } from "./headless/ListingActions.js";
|
|
232
|
+
export type { ListingActionsBag } from "./headless/ListingActions.js";
|
|
233
|
+
|
|
234
|
+
// ── nav manifest (the pair's public surface declaration) ─────────────────────
|
|
235
|
+
export { ACCOUNT_ROOT_ID, navEntries } from "./nav/manifest.js";
|
|
236
|
+
|
|
237
|
+
// ── i18n ─────────────────────────────────────────────────────────────────────
|
|
238
|
+
export {
|
|
239
|
+
LISTINGS_I18N_KEYS,
|
|
240
|
+
listingsI18nBundleEn,
|
|
241
|
+
registerListingsI18n,
|
|
242
|
+
} from "./i18n/keys.js";
|
|
243
|
+
export type { ListingsI18nKey } from "./i18n/keys.js";
|
|
244
|
+
|
|
245
|
+
// ── errors map (code → status/params/remediation/en; generated) ──────────────
|
|
246
|
+
export {
|
|
247
|
+
LISTINGS_ERRORS,
|
|
248
|
+
LISTINGS_ERROR_CODES,
|
|
249
|
+
LISTING_CANNOT_DELETE_ACTIVE,
|
|
250
|
+
LISTING_INVALID_TRANSITION,
|
|
251
|
+
LISTING_NOT_OWNER,
|
|
252
|
+
LISTING_PUBLISH_VALIDATION_FAILED,
|
|
253
|
+
explainListingsError,
|
|
254
|
+
listingsErrorBundleEn,
|
|
255
|
+
} from "./i18n/errorsMap.js";
|
|
256
|
+
export type {
|
|
257
|
+
ListingsErrorCode,
|
|
258
|
+
ListingsErrorSpec,
|
|
259
|
+
Remediation,
|
|
260
|
+
} from "./i18n/errorsMap.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Context } from "react";
|
|
2
|
+
import { createModuleContext } from "@stapel/core";
|
|
3
|
+
import type { Analytics, ModuleContextKit } from "@stapel/core";
|
|
4
|
+
import type { ListingsApi } from "../api/listingsApi.js";
|
|
5
|
+
import type { ListingsRuntime } from "./runtime.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The wired ListingsRuntime shared through React context by
|
|
9
|
+
* `<ListingsProvider>`. Hooks in `model/` and `headless/` read the singletons
|
|
10
|
+
* from here. One reviewed copy of this plumbing lives in `@stapel/core`
|
|
11
|
+
* (`createModuleContext`, slim wave §21/S2); this module binds it under the
|
|
12
|
+
* pair's public names.
|
|
13
|
+
*/
|
|
14
|
+
const kit: ModuleContextKit<ListingsRuntime> =
|
|
15
|
+
createModuleContext<ListingsRuntime>("Listings");
|
|
16
|
+
|
|
17
|
+
export const ListingsRuntimeContext: Context<ListingsRuntime | null> =
|
|
18
|
+
kit.RuntimeContext;
|
|
19
|
+
|
|
20
|
+
export const useListingsRuntime: () => ListingsRuntime = kit.useRuntime;
|
|
21
|
+
|
|
22
|
+
export const useListingsApi: () => ListingsApi = kit.useApi;
|
|
23
|
+
|
|
24
|
+
export const useListingsAnalytics: () => Analytics | null = kit.useAnalytics;
|
|
25
|
+
|
|
26
|
+
/** @internal Re-exported as `<ListingsProvider>` from `headless/`. */
|
|
27
|
+
export const ModuleProvider: ModuleContextKit<ListingsRuntime>["Provider"] =
|
|
28
|
+
kit.Provider;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The draft a composer holds, and its two conversions to the wire.
|
|
3
|
+
*
|
|
4
|
+
* stapel-listings stores every user-editable field TWICE: `title_draft` beside
|
|
5
|
+
* `title`, `images_draft` beside `images`, and so on. `publish` promotes the
|
|
6
|
+
* draft half onto the published half; nothing else does. So a composer edits
|
|
7
|
+
* exactly one side of that twin, and this module is where the browser's
|
|
8
|
+
* shape (plain values keyed the way a form keys them) meets the wire's
|
|
9
|
+
* (`*_draft` fields plus the `{slug: {type, value}}` feature envelope).
|
|
10
|
+
*
|
|
11
|
+
* Everything here is PURE — no React, no fetch — so the round trip is
|
|
12
|
+
* testable without mounting anything, and so `test/compose.test.tsx` can
|
|
13
|
+
* assert the BODY a save sends rather than the fact that it sent one.
|
|
14
|
+
*/
|
|
15
|
+
import type { FeatureDef, FeaturesDto } from "@stapel/attributes-react";
|
|
16
|
+
import { fromFeaturesDto, toFeaturesDto } from "@stapel/attributes-react";
|
|
17
|
+
import type {
|
|
18
|
+
ListingDetail,
|
|
19
|
+
ListingDraft,
|
|
20
|
+
ListingDraftPatch,
|
|
21
|
+
} from "../api/types.js";
|
|
22
|
+
import { DEFAULT_LISTING_CURRENCY } from "../api/types.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Where the listing is, as ONE value.
|
|
26
|
+
*
|
|
27
|
+
* A composite because the four members only make sense together: a `lat`
|
|
28
|
+
* without a `lon` is not "half a location", it is a broken one, and a
|
|
29
|
+
* `geohash` that disagrees with the coordinates beside it is worse than no
|
|
30
|
+
* geohash — the pin and the bucket point at different places.
|
|
31
|
+
*
|
|
32
|
+
* The pair does NOT compute `geohash` from `lat`/`lon`, and that is a
|
|
33
|
+
* decision. A geohash is precision-dependent, the indexer's bucketing depends
|
|
34
|
+
* on which precision was used, and a client that picked its own would put
|
|
35
|
+
* listings in neighbouring cells for reasons nobody could see. It is written
|
|
36
|
+
* by whatever resolved the place (a geo picker, `stapel-geo`) and carried
|
|
37
|
+
* verbatim from there to the wire.
|
|
38
|
+
*/
|
|
39
|
+
export interface ListingLocation {
|
|
40
|
+
/** Opaque id from whatever place directory the deployment uses. */
|
|
41
|
+
readonly locationId: string;
|
|
42
|
+
/** What a person reads: "Kazan, Vahitovsky district". */
|
|
43
|
+
readonly locationLabel: string;
|
|
44
|
+
/** Decimal STRING, as the wire spells it — a float here would round-trip
|
|
45
|
+
* `55.796100` into `55.7961` and change what was submitted. */
|
|
46
|
+
readonly lat: string | null;
|
|
47
|
+
readonly lon: string | null;
|
|
48
|
+
readonly geohash: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const EMPTY_LOCATION: ListingLocation = {
|
|
52
|
+
locationId: "",
|
|
53
|
+
locationLabel: "",
|
|
54
|
+
lat: null,
|
|
55
|
+
lon: null,
|
|
56
|
+
geohash: "",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Everything a composer edits.
|
|
61
|
+
*
|
|
62
|
+
* `features` holds PLAIN values keyed by slug — what a value editor's
|
|
63
|
+
* `onChange` produces — never the DTO envelope. The envelope is built at send
|
|
64
|
+
* time by `toFeaturesDto`, which tags each value with its type from the
|
|
65
|
+
* category schema; a composer that stored tagged values would have to
|
|
66
|
+
* re-tag them on every category change.
|
|
67
|
+
*/
|
|
68
|
+
export interface ListingDraftValues {
|
|
69
|
+
readonly categoryId: string;
|
|
70
|
+
readonly title: string;
|
|
71
|
+
readonly description: string;
|
|
72
|
+
/** Decimal string, or `""` for "no price yet". */
|
|
73
|
+
readonly price: string;
|
|
74
|
+
readonly currency: string;
|
|
75
|
+
readonly language: string;
|
|
76
|
+
/** CDN references (`<type>/<hash>`) — ORDER IS THE GALLERY ORDER, and the
|
|
77
|
+
* first one is the cover. This is exactly `bag.refs` from
|
|
78
|
+
* `@stapel/cdn-react`'s upload queue; see `headless/ListingComposer.tsx`. */
|
|
79
|
+
readonly images: readonly string[];
|
|
80
|
+
readonly location: ListingLocation;
|
|
81
|
+
readonly features: Readonly<Record<string, unknown>>;
|
|
82
|
+
/** A countable good (a quantity applies) vs a service. */
|
|
83
|
+
readonly countable: boolean;
|
|
84
|
+
readonly stockQuantity: number | null;
|
|
85
|
+
readonly autoRepublish: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface EmptyDraftOptions {
|
|
89
|
+
readonly categoryId?: string;
|
|
90
|
+
readonly currency?: string;
|
|
91
|
+
readonly language?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A blank draft. `currency` defaults to the storefront's (owner verdict F6 —
|
|
95
|
+
* RUB), overridable per runtime and per call. */
|
|
96
|
+
export function emptyDraftValues(
|
|
97
|
+
options: EmptyDraftOptions = {}
|
|
98
|
+
): ListingDraftValues {
|
|
99
|
+
return {
|
|
100
|
+
categoryId: options.categoryId ?? "",
|
|
101
|
+
title: "",
|
|
102
|
+
description: "",
|
|
103
|
+
price: "",
|
|
104
|
+
currency: options.currency ?? DEFAULT_LISTING_CURRENCY,
|
|
105
|
+
language: options.language ?? "",
|
|
106
|
+
images: [],
|
|
107
|
+
location: EMPTY_LOCATION,
|
|
108
|
+
features: {},
|
|
109
|
+
countable: false,
|
|
110
|
+
stockQuantity: null,
|
|
111
|
+
autoRepublish: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function text(value: string | null | undefined): string {
|
|
116
|
+
return value ?? "";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A reopened draft: the server's `*_draft` fields → the composer's values.
|
|
121
|
+
*
|
|
122
|
+
* `features_draft` is `null` on a listing nobody has filled in yet, which is
|
|
123
|
+
* not the same as `{}` on the wire but is the same thing to a composer.
|
|
124
|
+
*/
|
|
125
|
+
export function draftValuesFromWire(
|
|
126
|
+
draft: ListingDraft,
|
|
127
|
+
options: EmptyDraftOptions = {}
|
|
128
|
+
): ListingDraftValues {
|
|
129
|
+
const base = emptyDraftValues(options);
|
|
130
|
+
return {
|
|
131
|
+
categoryId: text(draft.category_id),
|
|
132
|
+
title: text(draft.title_draft),
|
|
133
|
+
description: text(draft.description_draft),
|
|
134
|
+
price: text(draft.price_draft),
|
|
135
|
+
currency: draft.currency !== undefined && draft.currency.length > 0
|
|
136
|
+
? draft.currency
|
|
137
|
+
: base.currency,
|
|
138
|
+
language: text(draft.language),
|
|
139
|
+
images: draft.images_draft ?? [],
|
|
140
|
+
location: {
|
|
141
|
+
locationId: text(draft.location_id_draft),
|
|
142
|
+
locationLabel: text(draft.location_label_draft),
|
|
143
|
+
lat: draft.lat_draft ?? null,
|
|
144
|
+
lon: draft.lon_draft ?? null,
|
|
145
|
+
geohash: text(draft.geohash_draft),
|
|
146
|
+
},
|
|
147
|
+
features: fromFeaturesDto((draft.features_draft ?? {}) as FeaturesDto),
|
|
148
|
+
countable: draft.countable ?? false,
|
|
149
|
+
stockQuantity: draft.stock_quantity ?? null,
|
|
150
|
+
autoRepublish: draft.auto_republish ?? false,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Editing something already PUBLISHED: the published half → the composer.
|
|
156
|
+
*
|
|
157
|
+
* A live listing's `*_draft` fields hold whatever was last submitted, which
|
|
158
|
+
* after a publish is the same content — but a listing published before this
|
|
159
|
+
* pair existed, or one whose draft was cleared, would open empty. Seeding
|
|
160
|
+
* from the PUBLISHED fields is what makes "edit" show the listing a person
|
|
161
|
+
* can actually see. Feature values come back from the DAO projection, which
|
|
162
|
+
* is the one place the published values live.
|
|
163
|
+
*/
|
|
164
|
+
export function draftValuesFromDetail(
|
|
165
|
+
detail: ListingDetail,
|
|
166
|
+
featuresDto: FeaturesDto,
|
|
167
|
+
options: EmptyDraftOptions = {}
|
|
168
|
+
): ListingDraftValues {
|
|
169
|
+
const base = emptyDraftValues(options);
|
|
170
|
+
return {
|
|
171
|
+
categoryId: detail.category_id,
|
|
172
|
+
title: text(detail.title),
|
|
173
|
+
description: text(detail.description),
|
|
174
|
+
price: text(detail.price),
|
|
175
|
+
currency: detail.currency !== undefined && detail.currency.length > 0
|
|
176
|
+
? detail.currency
|
|
177
|
+
: base.currency,
|
|
178
|
+
language: text(detail.language),
|
|
179
|
+
images: detail.images ?? [],
|
|
180
|
+
location: {
|
|
181
|
+
locationId: text(detail.location_id),
|
|
182
|
+
locationLabel: text(detail.location_label),
|
|
183
|
+
lat: detail.lat ?? null,
|
|
184
|
+
lon: detail.lon ?? null,
|
|
185
|
+
geohash: text(detail.geohash),
|
|
186
|
+
},
|
|
187
|
+
features: fromFeaturesDto(featuresDto),
|
|
188
|
+
countable: detail.countable ?? false,
|
|
189
|
+
stockQuantity: detail.stock_quantity ?? null,
|
|
190
|
+
autoRepublish: detail.auto_republish ?? false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The wire's shape for `features_draft`: since stapel-listings 0.6.1 fixed
|
|
195
|
+
* the discriminator, this is a map of properly discriminated DTOs. */
|
|
196
|
+
type WireFeaturesDraft = NonNullable<ListingDraftPatch["features_draft"]>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The ONE boundary between the two descriptions of a feature value.
|
|
200
|
+
*
|
|
201
|
+
* `toFeaturesDto` speaks the engine's own structural shape (`{type: string,
|
|
202
|
+
* value: unknown}` plus whatever a type adds) — it has to, because the type
|
|
203
|
+
* vocabulary is an OPEN registry server-side (`EXTRA_TYPES`, runtime
|
|
204
|
+
* `register_feature_type`), so a deployment can legitimately submit a type
|
|
205
|
+
* this generated union has never heard of. The generated union describes the
|
|
206
|
+
* ten BUILTIN types precisely, which is exactly right for reading and one
|
|
207
|
+
* notch too narrow for writing.
|
|
208
|
+
*
|
|
209
|
+
* Rather than widen the wire type or narrow the engine's, the conversion is
|
|
210
|
+
* named here, once, with the reason attached. A cast scattered at call sites
|
|
211
|
+
* would be the same code with nowhere to write this down.
|
|
212
|
+
*/
|
|
213
|
+
function toWireFeatures(
|
|
214
|
+
features: readonly FeatureDef[],
|
|
215
|
+
values: Readonly<Record<string, unknown>>
|
|
216
|
+
): WireFeaturesDraft {
|
|
217
|
+
return toFeaturesDto(features, values) as unknown as WireFeaturesDraft;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The composer's values → the `save-draft` body.
|
|
222
|
+
*
|
|
223
|
+
* `features` needs the category schema to be tagged, so it is passed in. A
|
|
224
|
+
* feature the schema no longer declares is dropped by `toFeaturesDto` only if
|
|
225
|
+
* it is absent from `features`; keeping a stale slug in the payload would be
|
|
226
|
+
* refused per-feature by 0.6.0's M-7 rule
|
|
227
|
+
* (`error.400.listing_feature_not_allowed`), which is why
|
|
228
|
+
* {@link retainKnownFeatureValues} prunes on the way in rather than letting
|
|
229
|
+
* the server explain it.
|
|
230
|
+
*/
|
|
231
|
+
export function draftPatchFromValues(
|
|
232
|
+
values: ListingDraftValues,
|
|
233
|
+
features: readonly FeatureDef[]
|
|
234
|
+
): ListingDraftPatch {
|
|
235
|
+
return {
|
|
236
|
+
category_id: values.categoryId,
|
|
237
|
+
title_draft: values.title,
|
|
238
|
+
description_draft: values.description,
|
|
239
|
+
price_draft: values.price.length > 0 ? values.price : null,
|
|
240
|
+
currency: values.currency,
|
|
241
|
+
...(values.language.length > 0 ? { language: values.language } : {}),
|
|
242
|
+
images_draft: [...values.images],
|
|
243
|
+
location_id_draft: values.location.locationId,
|
|
244
|
+
location_label_draft: values.location.locationLabel,
|
|
245
|
+
geohash_draft: values.location.geohash,
|
|
246
|
+
lat_draft: values.location.lat,
|
|
247
|
+
lon_draft: values.location.lon,
|
|
248
|
+
features_draft: toWireFeatures(features, values.features),
|
|
249
|
+
countable: values.countable,
|
|
250
|
+
// The pair mirrors the model's cross-field rule rather than sending a
|
|
251
|
+
// contradiction: a service carries no quantity, and `validate_countable
|
|
252
|
+
// _stock` refuses a `stock_quantity` beside `countable: false`.
|
|
253
|
+
stock_quantity: values.countable ? values.stockQuantity : null,
|
|
254
|
+
auto_republish: values.autoRepublish,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The body for CREATING a draft: `category_id` and nothing else.
|
|
260
|
+
*
|
|
261
|
+
* `perform_create` forces `owner` and `status`, and everything else has a
|
|
262
|
+
* model default, so a create that also carried the form's current contents
|
|
263
|
+
* would be a second write of data the very next `save-draft` sends anyway —
|
|
264
|
+
* and would fail the whole submission on a field the person could still fix.
|
|
265
|
+
* Create the row, then save into it.
|
|
266
|
+
*/
|
|
267
|
+
export function createDraftBody(categoryId: string): ListingDraftPatch {
|
|
268
|
+
return { category_id: categoryId };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Switching category: keep the answers whose slug the new schema also
|
|
273
|
+
* declares, drop the rest.
|
|
274
|
+
*
|
|
275
|
+
* Spec §4.1 asks for exactly this, and the reason is the same one the forms
|
|
276
|
+
* spec gives for `error.409.forms_version_superseded`: a person who picked
|
|
277
|
+
* "Phones", filled in Brand and Condition, then realised they meant "Used
|
|
278
|
+
* phones" should not retype what both categories ask for. A value whose slug
|
|
279
|
+
* is gone IS dropped, because 0.6.0 rejects an unknown slug per feature —
|
|
280
|
+
* carrying it would turn a category change into a publish refusal about a
|
|
281
|
+
* field the composer no longer draws.
|
|
282
|
+
*/
|
|
283
|
+
export function retainKnownFeatureValues(
|
|
284
|
+
values: Readonly<Record<string, unknown>>,
|
|
285
|
+
features: readonly FeatureDef[]
|
|
286
|
+
): Readonly<Record<string, unknown>> {
|
|
287
|
+
const known = new Set(features.map((feature) => feature.slug));
|
|
288
|
+
const out: Record<string, unknown> = {};
|
|
289
|
+
for (const [slug, value] of Object.entries(values)) {
|
|
290
|
+
if (known.has(slug)) out[slug] = value;
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Slugs that were answered and are NOT in the new schema — what
|
|
296
|
+
* {@link retainKnownFeatureValues} just dropped. A composer tells the person
|
|
297
|
+
* ("2 answers do not apply to this category") instead of losing them
|
|
298
|
+
* silently. */
|
|
299
|
+
export function droppedFeatureSlugs(
|
|
300
|
+
values: Readonly<Record<string, unknown>>,
|
|
301
|
+
features: readonly FeatureDef[]
|
|
302
|
+
): readonly string[] {
|
|
303
|
+
const known = new Set(features.map((feature) => feature.slug));
|
|
304
|
+
return Object.keys(values)
|
|
305
|
+
.filter((slug) => !known.has(slug))
|
|
306
|
+
.sort();
|
|
307
|
+
}
|