@stapel/reviews-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 +151 -0
- package/README.md +207 -0
- package/dist/api/generated/schema.d.ts +360 -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/reviewsApi.d.ts +89 -0
- package/dist/api/reviewsApi.d.ts.map +1 -0
- package/dist/api/reviewsApi.js +51 -0
- package/dist/api/reviewsApi.js.map +1 -0
- package/dist/api/types.d.ts +127 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -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/RatingBadge.d.ts +17 -0
- package/dist/default/RatingBadge.d.ts.map +1 -0
- package/dist/default/RatingBadge.js +23 -0
- package/dist/default/RatingBadge.js.map +1 -0
- package/dist/default/ReviewFormCard.d.ts +25 -0
- package/dist/default/ReviewFormCard.d.ts.map +1 -0
- package/dist/default/ReviewFormCard.js +42 -0
- package/dist/default/ReviewFormCard.js.map +1 -0
- package/dist/default/ReviewListPanel.d.ts +29 -0
- package/dist/default/ReviewListPanel.d.ts.map +1 -0
- package/dist/default/ReviewListPanel.js +47 -0
- package/dist/default/ReviewListPanel.js.map +1 -0
- package/dist/default/ReviewsPanel.d.ts +33 -0
- package/dist/default/ReviewsPanel.d.ts.map +1 -0
- package/dist/default/ReviewsPanel.js +17 -0
- package/dist/default/ReviewsPanel.js.map +1 -0
- package/dist/default/index.d.ts +31 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +25 -0
- package/dist/default/index.js.map +1 -0
- package/dist/default/theme.d.ts +24 -0
- package/dist/default/theme.d.ts.map +1 -0
- package/dist/default/theme.js +39 -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/headless/ReviewAggregate.d.ts +63 -0
- package/dist/headless/ReviewAggregate.d.ts.map +1 -0
- package/dist/headless/ReviewAggregate.js +55 -0
- package/dist/headless/ReviewAggregate.js.map +1 -0
- package/dist/headless/ReviewForm.d.ts +83 -0
- package/dist/headless/ReviewForm.d.ts.map +1 -0
- package/dist/headless/ReviewForm.js +75 -0
- package/dist/headless/ReviewForm.js.map +1 -0
- package/dist/headless/ReviewList.d.ts +47 -0
- package/dist/headless/ReviewList.d.ts.map +1 -0
- package/dist/headless/ReviewList.js +51 -0
- package/dist/headless/ReviewList.js.map +1 -0
- package/dist/headless/ReviewsProvider.d.ts +19 -0
- package/dist/headless/ReviewsProvider.d.ts.map +1 -0
- package/dist/headless/ReviewsProvider.js +15 -0
- package/dist/headless/ReviewsProvider.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +12 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +22 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +19 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +66 -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 +341 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +174 -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 +59 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +115 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +22 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +70 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +92 -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/list.d.ts +52 -0
- package/dist/model/list.d.ts.map +1 -0
- package/dist/model/list.js +60 -0
- package/dist/model/list.js.map +1 -0
- package/dist/model/mutations.d.ts +32 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +44 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +65 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +80 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +27 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +14 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/rating.d.ts +57 -0
- package/dist/model/rating.d.ts.map +1 -0
- package/dist/model/rating.js +47 -0
- package/dist/model/rating.js.map +1 -0
- package/dist/model/refusals.d.ts +51 -0
- package/dist/model/refusals.d.ts.map +1 -0
- package/dist/model/refusals.js +103 -0
- package/dist/model/refusals.js.map +1 -0
- package/dist/model/runtime.d.ts +44 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +15 -0
- package/dist/model/runtime.js.map +1 -0
- package/llms.txt +80 -0
- package/manifest.json +577 -0
- package/package.json +115 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/generated/schema.ts +362 -0
- package/src/api/reviewsApi.ts +170 -0
- package/src/api/types.ts +140 -0
- package/src/default/ErrorAlert.tsx +46 -0
- package/src/default/RatingBadge.tsx +81 -0
- package/src/default/ReviewFormCard.tsx +139 -0
- package/src/default/ReviewListPanel.tsx +204 -0
- package/src/default/ReviewsPanel.tsx +74 -0
- package/src/default/index.ts +30 -0
- package/src/default/theme.tsx +66 -0
- package/src/default/types.ts +12 -0
- package/src/headless/ReviewAggregate.tsx +94 -0
- package/src/headless/ReviewForm.tsx +163 -0
- package/src/headless/ReviewList.tsx +99 -0
- package/src/headless/ReviewsProvider.tsx +20 -0
- package/src/i18n/errorsMap.ts +33 -0
- package/src/i18n/es.ts +81 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +205 -0
- package/src/i18n/generated/errors.json +387 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +133 -0
- package/src/i18n/ru.ts +85 -0
- package/src/index.ts +161 -0
- package/src/model/context.tsx +28 -0
- package/src/model/list.ts +74 -0
- package/src/model/mutations.ts +62 -0
- package/src/model/queries.ts +122 -0
- package/src/model/queryKeys.ts +45 -0
- package/src/model/rating.ts +101 -0
- package/src/model/refusals.ts +114 -0
- package/src/model/runtime.ts +61 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Namespaced TanStack Query keys (frontend-standard §2). Everything under the
|
|
3
|
+
* `"reviews"` root so a host can invalidate the whole module, one target, or
|
|
4
|
+
* one read.
|
|
5
|
+
*
|
|
6
|
+
* THE TARGET IS TWO STRINGS, AND BOTH ARE IN THE KEY. `target_key` alone is
|
|
7
|
+
* not an identity: the module groups by the PAIR, and two deployments'
|
|
8
|
+
* registries can key different things with the same opaque string (a listing
|
|
9
|
+
* `"42"` and a course `"42"`). A key built from `target_key` only would serve
|
|
10
|
+
* one target's reviews for the other.
|
|
11
|
+
*
|
|
12
|
+
* `include` is part of the list key for the same reason: `include=all` is a
|
|
13
|
+
* DIFFERENT set of rows (pending and hidden ones), and folding it into the
|
|
14
|
+
* published-only entry would let a moderator's window overwrite what every
|
|
15
|
+
* other reader sees.
|
|
16
|
+
*/
|
|
17
|
+
import type { ReviewTarget } from "../api/types.js";
|
|
18
|
+
|
|
19
|
+
const ROOT = "reviews" as const;
|
|
20
|
+
|
|
21
|
+
export const reviewsQueryKeys: {
|
|
22
|
+
readonly all: readonly ["reviews"];
|
|
23
|
+
/** Everything cached about one target. */
|
|
24
|
+
target(target: ReviewTarget): readonly ["reviews", string, string];
|
|
25
|
+
/** The anchor-paginated list window for one target and one visibility scope. */
|
|
26
|
+
list(
|
|
27
|
+
target: ReviewTarget,
|
|
28
|
+
include?: "all"
|
|
29
|
+
): readonly ["reviews", string, string, "list", string];
|
|
30
|
+
/** The module-owned aggregate for one target. */
|
|
31
|
+
aggregate(
|
|
32
|
+
target: ReviewTarget
|
|
33
|
+
): readonly ["reviews", string, string, "aggregate"];
|
|
34
|
+
} = {
|
|
35
|
+
all: [ROOT],
|
|
36
|
+
target: (target) => [ROOT, target.targetType, target.targetKey],
|
|
37
|
+
list: (target, include) => [
|
|
38
|
+
ROOT,
|
|
39
|
+
target.targetType,
|
|
40
|
+
target.targetKey,
|
|
41
|
+
"list",
|
|
42
|
+
include ?? "published",
|
|
43
|
+
],
|
|
44
|
+
aggregate: (target) => [ROOT, target.targetType, target.targetKey, "aggregate"],
|
|
45
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a rating MEANS, kept in one pure module so a card, a panel and a
|
|
3
|
+
* seller header cannot each decide differently.
|
|
4
|
+
*
|
|
5
|
+
* The whole file exists for one line of the contract: `avg` is `0.0` when
|
|
6
|
+
* `count` is `0` (`services.aggregate`, and `AggregateResponse`'s own
|
|
7
|
+
* description says so). Rendered naively that is a zero-star rating — the
|
|
8
|
+
* worst possible score — printed over a target nobody has rated yet. It is the
|
|
9
|
+
* same class of defect as `data ?? []`: a value that was never measured
|
|
10
|
+
* displayed as if it had been measured and found to be nothing.
|
|
11
|
+
*
|
|
12
|
+
* So {@link ratingSummary} answers `rated: false` for that case and the star
|
|
13
|
+
* row has nothing to draw. There is no way to get an `avg` out of this module
|
|
14
|
+
* without also getting the flag that says whether it means anything.
|
|
15
|
+
*/
|
|
16
|
+
import type { RatingAggregate } from "../api/types.js";
|
|
17
|
+
|
|
18
|
+
/** An aggregate, read. */
|
|
19
|
+
export type RatingSummary =
|
|
20
|
+
| {
|
|
21
|
+
/** Nobody has published a review of this target. */
|
|
22
|
+
readonly rated: false;
|
|
23
|
+
readonly count: 0;
|
|
24
|
+
/** Deliberately absent: there is no average of an empty set. */
|
|
25
|
+
readonly avg?: undefined;
|
|
26
|
+
readonly rounded?: undefined;
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
readonly rated: true;
|
|
30
|
+
readonly count: number;
|
|
31
|
+
/** Mean over published reviews, as the server computed it (3 decimals). */
|
|
32
|
+
readonly avg: number;
|
|
33
|
+
/** The same mean at display precision — one decimal, half-up. */
|
|
34
|
+
readonly rounded: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Round half-up to one decimal for display. `Math.round` is already half-up
|
|
39
|
+
* for positives and ratings are positive, but the float dance is written out
|
|
40
|
+
* so 4.25 → 4.3 rather than 4.2 (binary representation makes
|
|
41
|
+
* `Math.round(4.25 * 10)` unreliable across engines otherwise).
|
|
42
|
+
*/
|
|
43
|
+
function oneDecimal(value: number): number {
|
|
44
|
+
return Math.round((value + Number.EPSILON) * 10) / 10;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read an aggregate — from this module's own `GET /reviews/aggregate`, or from
|
|
49
|
+
* the composite's `shop.listing_review_summary` projection, which answers the
|
|
50
|
+
* same two field names on purpose (`stapel_shop/projections.py`: "the owner's
|
|
51
|
+
* names ARE the contract shape here").
|
|
52
|
+
*
|
|
53
|
+
* A `count` of 0 is "not rated", whatever `avg` says. A negative or
|
|
54
|
+
* non-finite `count` is treated the same way rather than trusted: this is the
|
|
55
|
+
* one place a bad number can enter the display layer.
|
|
56
|
+
*/
|
|
57
|
+
export function ratingSummary(
|
|
58
|
+
aggregate: RatingAggregate | null | undefined
|
|
59
|
+
): RatingSummary {
|
|
60
|
+
const count = aggregate?.count;
|
|
61
|
+
if (
|
|
62
|
+
aggregate === null ||
|
|
63
|
+
aggregate === undefined ||
|
|
64
|
+
typeof count !== "number" ||
|
|
65
|
+
!Number.isFinite(count) ||
|
|
66
|
+
count <= 0
|
|
67
|
+
) {
|
|
68
|
+
return { rated: false, count: 0 };
|
|
69
|
+
}
|
|
70
|
+
const avg = Number.isFinite(aggregate.avg) ? aggregate.avg : 0;
|
|
71
|
+
return { rated: true, count, avg, rounded: oneDecimal(avg) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* How many whole, half and empty stars a `rounded` average draws, for a skin
|
|
76
|
+
* that renders stars rather than a number.
|
|
77
|
+
*
|
|
78
|
+
* `max` is the deployment's `RATING_MAX` (the runtime's `ratingBounds.max`),
|
|
79
|
+
* not a hardcoded five — a 1..10 deployment draws ten.
|
|
80
|
+
*/
|
|
81
|
+
export interface StarBreakdown {
|
|
82
|
+
readonly full: number;
|
|
83
|
+
readonly half: 0 | 1;
|
|
84
|
+
readonly empty: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function starBreakdown(rounded: number, max: number): StarBreakdown {
|
|
88
|
+
const clamped = Math.min(Math.max(rounded, 0), max);
|
|
89
|
+
const full = Math.floor(clamped);
|
|
90
|
+
const remainder = clamped - full;
|
|
91
|
+
// A remainder of a quarter or more is a half star; anything less rounds
|
|
92
|
+
// away. The alternative — a partial-width star — is a skin's business, and
|
|
93
|
+
// this breakdown stays a count so a non-antd skin can use it too.
|
|
94
|
+
const half: 0 | 1 = remainder >= 0.25 && remainder < 0.75 ? 1 : 0;
|
|
95
|
+
const rounded_up = remainder >= 0.75 ? 1 : 0;
|
|
96
|
+
return {
|
|
97
|
+
full: full + rounded_up,
|
|
98
|
+
half,
|
|
99
|
+
empty: max - full - rounded_up - half,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The refusals this pair has to tell apart, and the reason they are read by
|
|
3
|
+
* CODE and never by status.
|
|
4
|
+
*
|
|
5
|
+
* ── The trap ───────────────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* "You have already reviewed this" is a **400**
|
|
8
|
+
* (`ERR_400_DUPLICATE_REVIEW = "error.400.reviews_duplicate_review"`,
|
|
9
|
+
* `views.ReviewListCreateView.post`), not the 409 the situation reads like.
|
|
10
|
+
* And the module DOES answer 409 — for something else entirely:
|
|
11
|
+
* `error.409.reviews_already_responded`, which says the owner's reply already
|
|
12
|
+
* exists. A form that branched on `status === 409` would therefore miss the
|
|
13
|
+
* duplicate every time and mis-handle a refusal about a different object.
|
|
14
|
+
*
|
|
15
|
+
* That is why every predicate here compares a `FlowError.code`, folded
|
|
16
|
+
* through core's `toFlowError`, and why the codes are exported as named
|
|
17
|
+
* constants rather than typed at each call site.
|
|
18
|
+
*
|
|
19
|
+
* ── The other one worth naming ─────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* All four stapel-reviews views are `IsAuthenticated`, so a signed-out
|
|
22
|
+
* visitor gets 401 on the review LIST and on the AGGREGATE. Read as a
|
|
23
|
+
* generic failure that becomes "no reviews yet" under any code that defaults
|
|
24
|
+
* an empty array — a well-reviewed seller shown as unreviewed to exactly the
|
|
25
|
+
* people who have not signed up yet. {@link isSignInRequired} exists so the
|
|
26
|
+
* list bag can carry that as its own state and the skin can say the true
|
|
27
|
+
* thing.
|
|
28
|
+
*/
|
|
29
|
+
import { isErrorCode, toFlowError } from "@stapel/core";
|
|
30
|
+
import type { FlowError } from "@stapel/core";
|
|
31
|
+
|
|
32
|
+
/** The author already reviewed this target (`one_per_author`). A **400**. */
|
|
33
|
+
export const REVIEWS_ERROR_DUPLICATE = "error.400.reviews_duplicate_review";
|
|
34
|
+
/** `target_type` is not in the host's registry — or one of the pair was missing. */
|
|
35
|
+
export const REVIEWS_ERROR_UNKNOWN_TARGET_TYPE =
|
|
36
|
+
"error.400.reviews_unknown_target_type";
|
|
37
|
+
/** The rating fell outside `[RATING_MIN, RATING_MAX]`. */
|
|
38
|
+
export const REVIEWS_ERROR_INVALID_RATING = "error.400.reviews_invalid_rating";
|
|
39
|
+
/** The type's `can_review` callback said no. */
|
|
40
|
+
export const REVIEWS_ERROR_CANNOT_REVIEW = "error.403.reviews_cannot_review";
|
|
41
|
+
/** The type's `can_moderate` callback said no (fail-closed when unset). */
|
|
42
|
+
export const REVIEWS_ERROR_CANNOT_MODERATE = "error.403.reviews_cannot_moderate";
|
|
43
|
+
/** No review with that id. */
|
|
44
|
+
export const REVIEWS_ERROR_NOT_FOUND = "error.404.reviews_review_not_found";
|
|
45
|
+
/** Responses are switched off for this target type. */
|
|
46
|
+
export const REVIEWS_ERROR_RESPONSE_NOT_ALLOWED =
|
|
47
|
+
"error.400.reviews_response_not_allowed";
|
|
48
|
+
/** The review already carries the owner's reply — the module's ONLY 409. */
|
|
49
|
+
export const REVIEWS_ERROR_ALREADY_RESPONDED =
|
|
50
|
+
"error.409.reviews_already_responded";
|
|
51
|
+
|
|
52
|
+
/** Core's cross-cutting key for an unauthenticated call. */
|
|
53
|
+
const HTTP_401_KEY = "stapel.http.401";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Named, not inlined — the cdn-react precedent, for the same reason
|
|
57
|
+
* `stapel/no-adhoc-401` gives: the rule bans a bare `=== 401` because that
|
|
58
|
+
* shape is how ad hoc refresh/redirect logic gets written outside core's one
|
|
59
|
+
* seam. What happens here is not that. Nothing is refreshed, retried or
|
|
60
|
+
* redirected; the status is merely CLASSIFIED so a public page can say "sign
|
|
61
|
+
* in to read the reviews" instead of "this seller has no reviews". The real
|
|
62
|
+
* 401 handling stays on the client's `onAuthRefresh` seam and never reaches
|
|
63
|
+
* this file.
|
|
64
|
+
*/
|
|
65
|
+
const HTTP_UNAUTHORIZED = 401;
|
|
66
|
+
|
|
67
|
+
/** Fold any thrown value into this pair's error dialect. */
|
|
68
|
+
export function toReviewsError(error: unknown): FlowError {
|
|
69
|
+
return toFlowError(error, "reviews.error.unknown");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* "You have already reviewed this target."
|
|
74
|
+
*
|
|
75
|
+
* Only ever true when the target type sets `one_per_author: true` — the
|
|
76
|
+
* registry default is `false` (`registry.resolve_policy`), and the catalogue
|
|
77
|
+
* shop preset turns it on for `listing`. A host whose policy allows several
|
|
78
|
+
* reviews will simply never see this.
|
|
79
|
+
*/
|
|
80
|
+
export function isDuplicateReview(error: unknown): boolean {
|
|
81
|
+
return isErrorCode(toReviewsError(error), REVIEWS_ERROR_DUPLICATE);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* "This read needs a signed-in account." True for the 401 every endpoint of
|
|
86
|
+
* this module answers to an anonymous caller.
|
|
87
|
+
*
|
|
88
|
+
* Both halves are checked: core maps an un-keyed 401 body to
|
|
89
|
+
* `stapel.http.401`, but a deployment that puts its own `localizable_error`
|
|
90
|
+
* in a 401 body would arrive with that key instead — and it is still a 401.
|
|
91
|
+
*/
|
|
92
|
+
export function isSignInRequired(error: unknown): boolean {
|
|
93
|
+
const flow = toReviewsError(error);
|
|
94
|
+
return flow.status === HTTP_UNAUTHORIZED || isErrorCode(flow, HTTP_401_KEY);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The host's `can_review` callback refused this author for this target. */
|
|
98
|
+
export function isReviewingForbidden(error: unknown): boolean {
|
|
99
|
+
return isErrorCode(toReviewsError(error), REVIEWS_ERROR_CANNOT_REVIEW);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The target type is not registered on this deployment.
|
|
104
|
+
*
|
|
105
|
+
* Worth its own predicate because the view answers it for TWO different
|
|
106
|
+
* situations: an unknown type, and a missing `target_type`/`target_key` on a
|
|
107
|
+
* GET (`views.py` — "a missing one is ERR_400_UNKNOWN_TARGET_TYPE, not an
|
|
108
|
+
* empty list/aggregate"). Either way it is a wiring fault in the host, not
|
|
109
|
+
* something the person reading the page can fix, which is why the skin shows
|
|
110
|
+
* it as an error rather than as an empty state.
|
|
111
|
+
*/
|
|
112
|
+
export function isUnknownTargetType(error: unknown): boolean {
|
|
113
|
+
return isErrorCode(toReviewsError(error), REVIEWS_ERROR_UNKNOWN_TARGET_TYPE);
|
|
114
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createModuleRuntime } from "@stapel/core";
|
|
2
|
+
import type { CreateModuleRuntimeOptions, ModuleRuntime } from "@stapel/core";
|
|
3
|
+
import { createReviewsApi } from "../api/reviewsApi.js";
|
|
4
|
+
import type { ReviewsApi } from "../api/reviewsApi.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Inclusive rating bounds. These mirror `STAPEL_REVIEWS["RATING_MIN"]` /
|
|
8
|
+
* `["RATING_MAX"]`, whose library defaults are 1 and 5 — and they are
|
|
9
|
+
* OVERRIDABLE here for the same reason cdn-react's size ceilings are: they
|
|
10
|
+
* are deployment knobs, not constants, and a hardwired client would refuse a
|
|
11
|
+
* rating the server would have accepted (a 1..10 deployment) or offer one it
|
|
12
|
+
* would refuse (a 1..3 one).
|
|
13
|
+
*
|
|
14
|
+
* There is no endpoint that reports them, so a host that moves the knob tells
|
|
15
|
+
* this pair too. The mirror is a UI affordance only: the star row draws
|
|
16
|
+
* `max - min + 1` choices, and the server remains the authority
|
|
17
|
+
* (`error.400.reviews_invalid_rating`).
|
|
18
|
+
*/
|
|
19
|
+
export interface ReviewRatingBounds {
|
|
20
|
+
readonly min: number;
|
|
21
|
+
readonly max: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** stapel-reviews' own defaults (`conf.DEFAULTS`). */
|
|
25
|
+
export const DEFAULT_RATING_BOUNDS: ReviewRatingBounds = { min: 1, max: 5 };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The wired reviews runtime — core's `ModuleRuntime` bound to this pair's API
|
|
29
|
+
* (slim wave §21/S2), plus the deployment's rating bounds. The returned
|
|
30
|
+
* `client` is what the host injects into core's `StapelConfigProvider` (as the
|
|
31
|
+
* default or the `"reviews"` module client), preserving the client-injection
|
|
32
|
+
* fork seam (frontend-standard §7.2).
|
|
33
|
+
*
|
|
34
|
+
* MEMBER-ONLY, AND THE PAIR SAYS SO. Unlike categories-react, every endpoint
|
|
35
|
+
* behind this runtime is `IsAuthenticated` — a signed-out visitor gets 401 on
|
|
36
|
+
* the review list and on the aggregate. The read hooks are therefore gated on
|
|
37
|
+
* `useActiveSessionReady()` (so a bootstrapping session is not mistaken for an
|
|
38
|
+
* anonymous one) and the 401 is surfaced as a named "sign in to see reviews"
|
|
39
|
+
* state instead of an empty list.
|
|
40
|
+
*/
|
|
41
|
+
export type ReviewsRuntime = ModuleRuntime<ReviewsApi> & {
|
|
42
|
+
readonly ratingBounds: ReviewRatingBounds;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface CreateReviewsRuntimeOptions extends CreateModuleRuntimeOptions {
|
|
46
|
+
/** Mirror of the deployment's `RATING_MIN`/`RATING_MAX`. Defaults to 1..5. */
|
|
47
|
+
readonly ratingBounds?: Partial<ReviewRatingBounds>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createReviewsRuntime(
|
|
51
|
+
options: CreateReviewsRuntimeOptions
|
|
52
|
+
): ReviewsRuntime {
|
|
53
|
+
const runtime = createModuleRuntime(createReviewsApi, options);
|
|
54
|
+
return {
|
|
55
|
+
...runtime,
|
|
56
|
+
ratingBounds: {
|
|
57
|
+
min: options.ratingBounds?.min ?? DEFAULT_RATING_BOUNDS.min,
|
|
58
|
+
max: options.ratingBounds?.max ?? DEFAULT_RATING_BOUNDS.max,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"_comment": "Self-contained on purpose: standalone-buildable per frontend-standard §7. Mirrors the root tsconfig.base.json settings.",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"exactOptionalPropertyTypes": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"isolatedDeclarations": true,
|
|
16
|
+
"verbatimModuleSyntax": true,
|
|
17
|
+
"declaration": true,
|
|
18
|
+
"declarationMap": true,
|
|
19
|
+
"sourceMap": true,
|
|
20
|
+
"skipLibCheck": true,
|
|
21
|
+
"forceConsistentCasingInFileNames": true,
|
|
22
|
+
"outDir": "dist",
|
|
23
|
+
"rootDir": "src"
|
|
24
|
+
},
|
|
25
|
+
"include": ["src"]
|
|
26
|
+
}
|