@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the composer knows before it asks, and what it does with the answer.
|
|
3
|
+
*
|
|
4
|
+
* Three jobs, and the boundaries between them are the point:
|
|
5
|
+
*
|
|
6
|
+
* 1. **The mirror.** Feature values are mirrored by
|
|
7
|
+
* `@stapel/attributes-react`'s `mirrorValidate` — the engine's own rules,
|
|
8
|
+
* the engine's own error keys, one implementation. This module adds only
|
|
9
|
+
* the checks that belong to LISTINGS rather than to a value type: the
|
|
10
|
+
* description length bounds and the price shape. Nothing here is a
|
|
11
|
+
* verdict; the server re-runs all of it.
|
|
12
|
+
*
|
|
13
|
+
* 2. **The split of the publish 400.** `POST /{pk}/publish/` answers a bad
|
|
14
|
+
* draft with a BARE `ValidationBatchResult` — no `localizable_error`, no
|
|
15
|
+
* envelope — so core wraps it as `stapel.http.400` with the batch sitting
|
|
16
|
+
* on `StapelApiError.body`. A promotion that fails afterwards answers the
|
|
17
|
+
* ordinary envelope (`error.400.publish_validation_failed`, or
|
|
18
|
+
* `error.400.image_required` where the deployment requires a photo). Two
|
|
19
|
+
* different 400s that mean different things; `publishRefusal` is the one
|
|
20
|
+
* place that tells them apart.
|
|
21
|
+
*
|
|
22
|
+
* 3. **Routing a refusal onto the control that caused it.**
|
|
23
|
+
* `featureErrorsBySlug` (attributes-react) adds the `field` param the
|
|
24
|
+
* fleet's `useFieldError` convention routes on — the engine sends
|
|
25
|
+
* `{feature, slug}` and never `field`. Listing-level rows (the
|
|
26
|
+
* description, which the batch carries under the slug `"description"`)
|
|
27
|
+
* are lifted out to their own control here.
|
|
28
|
+
*
|
|
29
|
+
* ── The mirror's ceilings are CONFIGURABLE, and that is not a nicety ───────
|
|
30
|
+
*
|
|
31
|
+
* `DESCRIPTION_MIN_LENGTH` / `DESCRIPTION_MAX_LENGTH` are `STAPEL_LISTINGS`
|
|
32
|
+
* settings a deployment moves (`conf.py`). A hardcoded 4/500 would refuse a
|
|
33
|
+
* perfectly valid submission on a host that widened them — and the server
|
|
34
|
+
* would never even hear about the refusal, so there would be nothing to
|
|
35
|
+
* appeal to. Same rule the cdn pair wrote down: **the mirror may not refuse
|
|
36
|
+
* what the server would accept.**
|
|
37
|
+
*/
|
|
38
|
+
import { StapelApiError, isStapelApiError } from "@stapel/core";
|
|
39
|
+
import { codePointLength, featureErrorsBySlug, mirrorValidate, } from "@stapel/attributes-react";
|
|
40
|
+
import { LISTINGS_I18N_KEYS } from "../i18n/keys.js";
|
|
41
|
+
export const DEFAULT_DRAFT_LIMITS = {
|
|
42
|
+
descriptionMin: 4,
|
|
43
|
+
descriptionMax: 500,
|
|
44
|
+
titleMax: 255,
|
|
45
|
+
maxImages: 10,
|
|
46
|
+
requireImageOnPublish: true,
|
|
47
|
+
};
|
|
48
|
+
/** The slug the description's refusal is filed under — the engine files it
|
|
49
|
+
* the same way (`validate_description` returns a `FeatureValidationResult`
|
|
50
|
+
* whose slug is not a feature's), so the composer has one routing table. */
|
|
51
|
+
export const DESCRIPTION_FIELD = "description";
|
|
52
|
+
export const TITLE_FIELD = "title";
|
|
53
|
+
export const PRICE_FIELD = "price";
|
|
54
|
+
export const IMAGES_FIELD = "images";
|
|
55
|
+
export const CATEGORY_FIELD = "category_id";
|
|
56
|
+
/** A client-side refusal. `status: 0` on purpose: a rule this build applied
|
|
57
|
+
* must never be indistinguishable from one that came over the wire. */
|
|
58
|
+
function mirrored(code, params = {}) {
|
|
59
|
+
return { code, params, status: 0, message: undefined, language: undefined };
|
|
60
|
+
}
|
|
61
|
+
const DECIMAL = /^\d{1,10}(?:\.\d{1,2})?$/;
|
|
62
|
+
/**
|
|
63
|
+
* The listing-level half of the mirror: everything that is not a feature
|
|
64
|
+
* value. Returns refusals keyed by control, empty when nothing is wrong.
|
|
65
|
+
*
|
|
66
|
+
* Note what is NOT judged: a missing title, a missing price and a missing
|
|
67
|
+
* location. The server accepts a published listing without any of them
|
|
68
|
+
* (`publish_listing` promotes `title_draft or title` and leaves the rest),
|
|
69
|
+
* so refusing them here would block a submission the backend would take. A
|
|
70
|
+
* skin may still *encourage* them; that is copy, not a gate.
|
|
71
|
+
*/
|
|
72
|
+
export function mirrorListingFields(values, limits) {
|
|
73
|
+
const out = {};
|
|
74
|
+
if (values.categoryId.length === 0) {
|
|
75
|
+
out[CATEGORY_FIELD] = mirrored(LISTINGS_I18N_KEYS.composeCategoryRequired);
|
|
76
|
+
}
|
|
77
|
+
if (codePointLength(values.title) > limits.titleMax) {
|
|
78
|
+
out[TITLE_FIELD] = mirrored(LISTINGS_I18N_KEYS.composeTitleTooLong, {
|
|
79
|
+
max_length: limits.titleMax,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Length in Unicode CODE POINTS, the unit Python's `len()` counts in. One
|
|
83
|
+
// emoji is one character on both sides of the wire; `String.length` would
|
|
84
|
+
// call it two and refuse a description the server accepts.
|
|
85
|
+
const described = codePointLength(values.description.trim());
|
|
86
|
+
if (described < limits.descriptionMin) {
|
|
87
|
+
out[DESCRIPTION_FIELD] = mirrored("error.400.description_too_short", {
|
|
88
|
+
min_length: limits.descriptionMin,
|
|
89
|
+
field: DESCRIPTION_FIELD,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
else if (described > limits.descriptionMax) {
|
|
93
|
+
out[DESCRIPTION_FIELD] = mirrored("error.400.description_too_long", {
|
|
94
|
+
max_length: limits.descriptionMax,
|
|
95
|
+
field: DESCRIPTION_FIELD,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (values.price.length > 0 && !DECIMAL.test(values.price)) {
|
|
99
|
+
// `validate_price_draft` refuses a negative, and the DecimalField refuses
|
|
100
|
+
// anything outside `^-?\d{0,10}(\.\d{0,2})?$`. One sentence for both:
|
|
101
|
+
// "that is not a price we can send".
|
|
102
|
+
out[PRICE_FIELD] = mirrored(LISTINGS_I18N_KEYS.composePriceInvalid);
|
|
103
|
+
}
|
|
104
|
+
if (values.images.length > limits.maxImages) {
|
|
105
|
+
out[IMAGES_FIELD] = mirrored(LISTINGS_I18N_KEYS.composeTooManyImages, {
|
|
106
|
+
max: limits.maxImages,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// The coordinates are a composite: half of one is not half a location.
|
|
110
|
+
const { lat, lon } = values.location;
|
|
111
|
+
const hasLat = lat !== null && lat.length > 0;
|
|
112
|
+
const hasLon = lon !== null && lon.length > 0;
|
|
113
|
+
if (hasLat !== hasLon) {
|
|
114
|
+
out["location"] = mirrored(LISTINGS_I18N_KEYS.composeGeoIncomplete);
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The whole mirror: listing fields + feature values, in one routing table.
|
|
120
|
+
*
|
|
121
|
+
* Feature refusals go through `featureErrorsBySlug`, so a mirrored "too long"
|
|
122
|
+
* and a server "too long" render the SAME sentence from the SAME key — they
|
|
123
|
+
* are the same key, because the mirror speaks the engine's vocabulary.
|
|
124
|
+
*/
|
|
125
|
+
export function mirrorDraft(values, features, featuresDto, limits) {
|
|
126
|
+
return {
|
|
127
|
+
...featureErrorsBySlug(mirrorValidate(features, featuresDto)),
|
|
128
|
+
...mirrorListingFields(values, limits),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Is this batch clean? `valid` is the server's own summary; a caller that
|
|
132
|
+
* recomputed it from `results` could disagree with the field the API sends. */
|
|
133
|
+
export function isBatchValid(batch) {
|
|
134
|
+
return batch.valid;
|
|
135
|
+
}
|
|
136
|
+
function isValidationBatch(value) {
|
|
137
|
+
if (typeof value !== "object" || value === null)
|
|
138
|
+
return false;
|
|
139
|
+
const candidate = value;
|
|
140
|
+
return typeof candidate.valid === "boolean" && Array.isArray(candidate.results);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Split a thrown publish failure into its two meanings.
|
|
144
|
+
*
|
|
145
|
+
* The check is on the BODY, not on the status: a 400 whose body is a batch is
|
|
146
|
+
* a per-field verdict, and a 400 whose body is an envelope is a sentence. A
|
|
147
|
+
* caller that branched on `status === 400` alone would put
|
|
148
|
+
* `error.400.image_required` under a feature control.
|
|
149
|
+
*/
|
|
150
|
+
export function publishRefusal(thrown) {
|
|
151
|
+
if (isStapelApiError(thrown) && isValidationBatch(thrown.body)) {
|
|
152
|
+
const batch = thrown.body;
|
|
153
|
+
return {
|
|
154
|
+
kind: "invalid_draft",
|
|
155
|
+
batch,
|
|
156
|
+
fieldErrors: listingFieldErrors(batch),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
kind: "error",
|
|
161
|
+
error: isStapelApiError(thrown)
|
|
162
|
+
? thrown
|
|
163
|
+
: new StapelApiError({
|
|
164
|
+
code: LISTINGS_I18N_KEYS.unknownError,
|
|
165
|
+
message: "Publishing failed",
|
|
166
|
+
status: 0,
|
|
167
|
+
body: thrown,
|
|
168
|
+
}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A server batch → refusals keyed by CONTROL.
|
|
173
|
+
*
|
|
174
|
+
* One call, because the server already keys everything the way the composer
|
|
175
|
+
* does. `services.publish.validate_draft` inserts `validate_description`'s
|
|
176
|
+
* row at the FRONT of the same list, and that row's slug is literally
|
|
177
|
+
* `"description"` (`stapel_attributes/validation.py:726`) — the same key
|
|
178
|
+
* {@link DESCRIPTION_FIELD} names and the same key the mirror files its own
|
|
179
|
+
* length refusal under. So the description's control receives the server's
|
|
180
|
+
* verdict and the mirror's through one lookup, and neither can land in a
|
|
181
|
+
* page-level banner because nothing routed it.
|
|
182
|
+
*/
|
|
183
|
+
export function listingFieldErrors(batch) {
|
|
184
|
+
return featureErrorsBySlug(batch);
|
|
185
|
+
}
|
|
186
|
+
/** The rows of a batch that failed, in the order the server sent them —
|
|
187
|
+
* for a summary line above the form ("3 details need attention"). */
|
|
188
|
+
export function failedResults(batch) {
|
|
189
|
+
return batch.results.filter((result) => result.status === "validation_failed");
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/model/validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This pair's contribution to the scripted-fullstack nav contract
|
|
3
|
+
* (`@stapel/core`'s `NavEntry`/`PackageNavManifest`). `scripts/gen-nav-
|
|
4
|
+
* manifest.mjs` reads `navEntries` below, stamps `package`/`version` from THIS
|
|
5
|
+
* package's own `package.json`, and emits
|
|
6
|
+
* `packages/listings-react/nav-manifest.json` plus this package's slice of the
|
|
7
|
+
* root aggregate.
|
|
8
|
+
*
|
|
9
|
+
* ── Four entries, and the one thing they are NOT ───────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* There is no entry for a card. A card is a SLOT, not a route (spec §3.7):
|
|
12
|
+
* `@stapel/search-react` takes `renderCard`, the container passes
|
|
13
|
+
* `<ListingCard>` from this pair's `/default`, and the two L2 pairs never
|
|
14
|
+
* import each other. A nav entry for it would claim a page that does not
|
|
15
|
+
* exist.
|
|
16
|
+
*
|
|
17
|
+
* `surface` is declared EXPLICITLY on all four. The derivation
|
|
18
|
+
* `requiresAuth ? "member" : "public"` lands on the same answer today, but
|
|
19
|
+
* the explicit declaration is what a public container can rely on: an entry
|
|
20
|
+
* that later gains `requiresAuth` for an unrelated reason must not silently
|
|
21
|
+
* fall out of the anonymous tree (`core/src/nav.ts`, `navEntrySurface`).
|
|
22
|
+
*
|
|
23
|
+
* ── Why the detail is public and the other three are not ───────────────────
|
|
24
|
+
*
|
|
25
|
+
* `/l/:id` is the page a shared link opens, the page a search result leads
|
|
26
|
+
* to, and the page a message quotes — a marketplace whose listing page needs
|
|
27
|
+
* a session has no shop window. It is spelled out in spec §5.2 with this
|
|
28
|
+
* exact path and this exact surface. `menuVisibleDefault: false` because a
|
|
29
|
+
* parameterized route is a navigation TARGET, not a menu item (the same
|
|
30
|
+
* treatment `auth.login` and `search.results` get).
|
|
31
|
+
*
|
|
32
|
+
* The other three need a mandate and say so. Two of them are SUBMENU entries
|
|
33
|
+
* under `account.root` — an id this pair does not own and does not declare:
|
|
34
|
+
* the cabinet has no module of its own, so the CONTAINER declares that top
|
|
35
|
+
* entry in its `stapel.nav.json` override. `resolveNav` drops an orphaned
|
|
36
|
+
* submenu entry instead of throwing (`NavPlacement.parentId`), so a host that
|
|
37
|
+
* installs this pair without a cabinet gets a smaller menu rather than a
|
|
38
|
+
* broken build.
|
|
39
|
+
*/
|
|
40
|
+
import type { NavEntry } from "@stapel/core";
|
|
41
|
+
/** The container-owned top entry the cabinet's submenu hangs from. Declared
|
|
42
|
+
* as a constant so the two references below cannot drift apart, and exported
|
|
43
|
+
* so a container can assert it against its own override file. */
|
|
44
|
+
export declare const ACCOUNT_ROOT_ID = "account.root";
|
|
45
|
+
export declare const navEntries: readonly NavEntry[];
|
|
46
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/nav/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C;;iEAEiE;AACjE,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAE9C,eAAO,MAAM,UAAU,EAAE,SAAS,QAAQ,EAkDzC,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** The container-owned top entry the cabinet's submenu hangs from. Declared
|
|
2
|
+
* as a constant so the two references below cannot drift apart, and exported
|
|
3
|
+
* so a container can assert it against its own override file. */
|
|
4
|
+
export const ACCOUNT_ROOT_ID = "account.root";
|
|
5
|
+
export const navEntries = [
|
|
6
|
+
{
|
|
7
|
+
id: "listings.detail",
|
|
8
|
+
labelKey: "listings.nav.detail",
|
|
9
|
+
icon: "TagOutlined",
|
|
10
|
+
route: { path: "/l/:id" },
|
|
11
|
+
component: { export: "ListingDetailPane", subpath: "default" },
|
|
12
|
+
placement: { level: "top" },
|
|
13
|
+
menuVisibleDefault: false,
|
|
14
|
+
requiresAuth: false,
|
|
15
|
+
surface: "public",
|
|
16
|
+
order: 10,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "listings.compose",
|
|
20
|
+
labelKey: "listings.nav.compose",
|
|
21
|
+
icon: "PlusOutlined",
|
|
22
|
+
route: { path: "/new" },
|
|
23
|
+
component: { export: "ListingComposerPage", subpath: "default" },
|
|
24
|
+
placement: { level: "top" },
|
|
25
|
+
menuVisibleDefault: true,
|
|
26
|
+
requiresAuth: true,
|
|
27
|
+
surface: "member",
|
|
28
|
+
order: 20,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
// Relative path: a child of the container's `/account` layout route.
|
|
32
|
+
id: "listings.mine",
|
|
33
|
+
labelKey: "listings.nav.mine",
|
|
34
|
+
icon: "ProfileOutlined",
|
|
35
|
+
route: { path: "listings" },
|
|
36
|
+
component: { export: "MyListingsPane", subpath: "default" },
|
|
37
|
+
placement: { level: "submenu", parentId: ACCOUNT_ROOT_ID },
|
|
38
|
+
menuVisibleDefault: true,
|
|
39
|
+
requiresAuth: true,
|
|
40
|
+
surface: "member",
|
|
41
|
+
order: 10,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "listings.favorites",
|
|
45
|
+
labelKey: "listings.nav.favorites",
|
|
46
|
+
icon: "HeartOutlined",
|
|
47
|
+
route: { path: "favorites" },
|
|
48
|
+
component: { export: "FavoritesPane", subpath: "default" },
|
|
49
|
+
placement: { level: "submenu", parentId: ACCOUNT_ROOT_ID },
|
|
50
|
+
menuVisibleDefault: true,
|
|
51
|
+
requiresAuth: true,
|
|
52
|
+
surface: "member",
|
|
53
|
+
order: 20,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/nav/manifest.ts"],"names":[],"mappings":"AAyCA;;iEAEiE;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,cAAc,CAAC;AAE9C,MAAM,CAAC,MAAM,UAAU,GAAwB;IAC7C;QACE,EAAE,EAAE,iBAAiB;QACrB,QAAQ,EAAE,qBAAqB;QAC/B,IAAI,EAAE,aAAa;QACnB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;QACzB,SAAS,EAAE,EAAE,MAAM,EAAE,mBAAmB,EAAE,OAAO,EAAE,SAAS,EAAE;QAC9D,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;QAC3B,kBAAkB,EAAE,KAAK;QACzB,YAAY,EAAE,KAAK;QACnB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,EAAE;KACV;IACD;QACE,EAAE,EAAE,kBAAkB;QACtB,QAAQ,EAAE,sBAAsB;QAChC,IAAI,EAAE,cAAc;QACpB,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;QACvB,SAAS,EAAE,EAAE,MAAM,EAAE,qBAAqB,EAAE,OAAO,EAAE,SAAS,EAAE;QAChE,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE;QAC3B,kBAAkB,EAAE,IAAI;QACxB,YAAY,EAAE,IAAI;QAClB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,EAAE;KACV;IACD;QACE,qEAAqE;QACrE,EAAE,EAAE,eAAe;QACnB,QAAQ,EAAE,mBAAmB;QAC7B,IAAI,EAAE,iBAAiB;QACvB,KAAK,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;QAC3B,SAAS,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,SAAS,EAAE;QAC3D,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1D,kBAAkB,EAAE,IAAI;QACxB,YAAY,EAAE,IAAI;QAClB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,EAAE;KACV;IACD;QACE,EAAE,EAAE,oBAAoB;QACxB,QAAQ,EAAE,wBAAwB;QAClC,IAAI,EAAE,eAAe;QACrB,KAAK,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;QAC5B,SAAS,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,SAAS,EAAE;QAC1D,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,eAAe,EAAE;QAC1D,kBAAkB,EAAE,IAAI;QACxB,YAAY,EAAE,IAAI;QAClB,OAAO,EAAE,QAAQ;QACjB,KAAK,EAAE,EAAE;KACV;CACF,CAAC"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @stapel/listings-react 0.1.0
|
|
2
|
+
|
|
3
|
+
Headless React flow pair for stapel-listings (contract >=0.6 <0.7) — business + state, zero visual opinion.
|
|
4
|
+
Built on @stapel/core: typed client + StapelApiError envelope, auth token refresh,
|
|
5
|
+
verification-403 interception, i18n engine, analytics facade, TanStack Query layer.
|
|
6
|
+
|
|
7
|
+
## The one right way (do this, the rest is a review/lint smell)
|
|
8
|
+
- No raw fetch/axios. The client is injected via <ListingsProvider>/StapelConfigProvider;
|
|
9
|
+
every hook and flow already carries auth, refresh, and the error envelope.
|
|
10
|
+
- Render errors, never try/catch them: a flow's state carries FlowError{code,params};
|
|
11
|
+
render `t(code, params)` and branch on `explainListingsError(code)` remediation.
|
|
12
|
+
- Server state = the use* hooks (query layer); keys come only from listingsQueryKeys.
|
|
13
|
+
- User strings = i18n keys (registerListingsI18n); never string literals.
|
|
14
|
+
- Sign-in UI = a headless flow component; copy it (shadcn-style) to restyle.
|
|
15
|
+
|
|
16
|
+
## Layers
|
|
17
|
+
api (typed client) · model (hooks, session) · flows (machines) · headless · i18n
|
|
18
|
+
|
|
19
|
+
## Documented flows (flows.json — canonical id, steps, endpoints)
|
|
20
|
+
|
|
21
|
+
## Operations (typed; use the named op, never a path string)
|
|
22
|
+
Request/response schema names are in manifest.json + the generated types.
|
|
23
|
+
Paths are relative to `/listings/api/v1/`.
|
|
24
|
+
- listings_api_v1_listings_archive_create: POST /listings/{id}/archive/
|
|
25
|
+
- listings_api_v1_listings_complete_create: POST /listings/{id}/complete/
|
|
26
|
+
- listings_api_v1_listings_create: POST /listings/
|
|
27
|
+
- listings_api_v1_listings_destroy: DELETE /listings/{id}/
|
|
28
|
+
- listings_api_v1_listings_favorite_create: POST /listings/{id}/favorite/
|
|
29
|
+
- listings_api_v1_listings_list: GET /listings/
|
|
30
|
+
- listings_api_v1_listings_my_counters_retrieve: GET /listings/my/counters/
|
|
31
|
+
- listings_api_v1_listings_my_favorites_list: GET /listings/my/favorites/
|
|
32
|
+
- listings_api_v1_listings_partial_update: PATCH /listings/{id}/
|
|
33
|
+
- listings_api_v1_listings_publish_create: POST /listings/{id}/publish/
|
|
34
|
+
- listings_api_v1_listings_retrieve: GET /listings/{id}/
|
|
35
|
+
- listings_api_v1_listings_save_draft_create: POST /listings/{id}/save-draft/
|
|
36
|
+
- listings_api_v1_listings_status_retrieve: GET /listings/{id}/status/
|
|
37
|
+
- listings_api_v1_listings_unfavorite_create: POST /listings/{id}/unfavorite/
|
|
38
|
+
- listings_api_v1_listings_update: PUT /listings/{id}/
|
|
39
|
+
- listings_api_v1_listings_validate_draft_retrieve: GET /listings/{id}/validate-draft/
|
|
40
|
+
|
|
41
|
+
## Query hooks (server state; keys come only from the key factory)
|
|
42
|
+
- useArchiveListing (mutation) → archive
|
|
43
|
+
- useCompleteListing (mutation) → complete
|
|
44
|
+
- useCreateDraft (mutation) → createDraft
|
|
45
|
+
- useDeleteListing (mutation) → remove
|
|
46
|
+
- useFavoriteListing (mutation) → favorite+unfavorite
|
|
47
|
+
- useListing (query) → retrieve
|
|
48
|
+
- useListingCards (query) → list
|
|
49
|
+
- useListingStatus (query) → status
|
|
50
|
+
- useMyCounters (query) → myCounters
|
|
51
|
+
- useMyFavorites (query) → myFavorites
|
|
52
|
+
- usePublishListing (mutation) → publish
|
|
53
|
+
- useSaveDraft (mutation) → saveDraft
|
|
54
|
+
- useValidateDraft (query) → validateDraft
|
|
55
|
+
|
|
56
|
+
## Errors (render t(code, params); UX from remediation)
|
|
57
|
+
63 keys (full catalog: manifest.json §errors). By remediation: fix_input 35 · retry 15 · verify 5 · wait_and_retry 5 · contact_support 2 · reauthenticate 1.
|
|
58
|
+
Param-bearing keys (interpolation slots matter):
|
|
59
|
+
- error.400.description_too_long [400] → fix_input {max_length}
|
|
60
|
+
- error.400.description_too_short [400] → fix_input {min_length}
|
|
61
|
+
- error.400.feature_above_maximum [400] → fix_input {feature}
|
|
62
|
+
- error.400.feature_below_minimum [400] → fix_input {feature}
|
|
63
|
+
- error.400.feature_invalid_config [400] → fix_input {feature}
|
|
64
|
+
- error.400.feature_invalid_format [400] → fix_input {feature}
|
|
65
|
+
- error.400.feature_invalid_type [400] → fix_input {feature}
|
|
66
|
+
- error.400.feature_mandatory_missing [400] → fix_input {feature}
|
|
67
|
+
- error.400.feature_not_allowed [400] → fix_input {feature}
|
|
68
|
+
- error.400.feature_not_in_options [400] → fix_input {feature}
|
|
69
|
+
- error.400.feature_unknown [400] → fix_input {feature}
|
|
70
|
+
- error.400.feature_unknown_type [400] → fix_input {feature}
|
|
71
|
+
- error.400.field.blank [400] → fix_input {field}
|
|
72
|
+
- error.400.field.does_not_exist [400] → fix_input {field}
|
|
73
|
+
- error.400.field.invalid [400] → fix_input {field}
|
|
74
|
+
- error.400.field.invalid_choice [400] → fix_input {field}
|
|
75
|
+
- error.400.field.max_length [400] → fix_input {field,max_length}
|
|
76
|
+
- error.400.field.max_value [400] → fix_input {field,max_value}
|
|
77
|
+
- error.400.field.min_length [400] → fix_input {field,min_length}
|
|
78
|
+
- error.400.field.min_value [400] → fix_input {field,min_value}
|
|
79
|
+
- error.400.field.null [400] → fix_input {field}
|
|
80
|
+
- error.400.field.required [400] → fix_input {field}
|
|
81
|
+
- error.400.field.unique [400] → fix_input {field}
|
|
82
|
+
- error.400.listing_feature_not_allowed [400] → fix_input {feature}
|
|
83
|
+
- error.409.invalid_listing_transition [409] → fix_input {from_status}
|
|
84
|
+
- error.429.rate_limit [429] → wait_and_retry {retry_after_minutes}
|
|
85
|
+
|
|
86
|
+
## Analytics events (typed; defineEvent → events.json, drift-gated)
|
|
87
|
+
- (no app defineEvent() in this pair — its analytic events are the
|
|
88
|
+
auto-instrumented flow funnels below)
|
|
89
|
+
|
|
90
|
+
## Flow funnels (auto-instrumented: flow.<id>.<step> {phase})
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// Typed event + tracked() click (the one right way; §3.1).
|
|
94
|
+
const planSelected = defineEvent({
|
|
95
|
+
name: "pricing.plan.selected",
|
|
96
|
+
description: "User picked a plan",
|
|
97
|
+
props: { plan: prop.oneOf(["free", "pro", "team"], "Plan code") },
|
|
98
|
+
});
|
|
99
|
+
const { tracked } = useTracked();
|
|
100
|
+
<Button onClick={tracked(planSelected, { plan }, startCheckout)} />
|
|
101
|
+
// A click that STEPS a flow machine is already instrumented — mark it
|
|
102
|
+
// data-analytics="flow" instead; tracked() on top double-counts (§3.2).
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Demos (defineDemo → manifest.demos; compiled, linted, rendered examples)
|
|
106
|
+
- listings.card → <ListingCard> [default|visitor] demo/ListingCard.demo.tsx
|
|
107
|
+
- listings.composer → <ListingComposerPage> [default|photos-in-flight|unsupported-type] demo/ListingComposer.demo.tsx
|
|
108
|
+
- listings.detail → <ListingDetailPane> [default|owner] demo/ListingDetail.demo.tsx
|
|
109
|
+
- listings.favorites → <FavoritesPane> [default|empty|failed|visitor] demo/Favorites.demo.tsx
|
|
110
|
+
- listings.mine → <MyListingsPane> [default|no-source] demo/MyListings.demo.tsx
|
|
111
|
+
Each source file is the canonical usage snippet (open the default variant).
|
|
112
|
+
|
|
113
|
+
## Snippets
|
|
114
|
+
```tsx
|
|
115
|
+
// Error rendering + remediation branch (one pattern for every pair).
|
|
116
|
+
const r = explainListingsError(err.code); // 'wait_and_retry' | 'verify' | ...
|
|
117
|
+
return <Alert action={r}>{t(err.code, err.params)}</Alert>;
|
|
118
|
+
```
|