@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +1 -0
  2. package/MODULE.md +151 -0
  3. package/README.md +207 -0
  4. package/dist/api/generated/schema.d.ts +360 -0
  5. package/dist/api/generated/schema.d.ts.map +1 -0
  6. package/dist/api/generated/schema.js +2 -0
  7. package/dist/api/generated/schema.js.map +1 -0
  8. package/dist/api/reviewsApi.d.ts +89 -0
  9. package/dist/api/reviewsApi.d.ts.map +1 -0
  10. package/dist/api/reviewsApi.js +51 -0
  11. package/dist/api/reviewsApi.js.map +1 -0
  12. package/dist/api/types.d.ts +127 -0
  13. package/dist/api/types.d.ts.map +1 -0
  14. package/dist/api/types.js +2 -0
  15. package/dist/api/types.js.map +1 -0
  16. package/dist/default/ErrorAlert.d.ts +9 -0
  17. package/dist/default/ErrorAlert.d.ts.map +1 -0
  18. package/dist/default/ErrorAlert.js +27 -0
  19. package/dist/default/ErrorAlert.js.map +1 -0
  20. package/dist/default/RatingBadge.d.ts +17 -0
  21. package/dist/default/RatingBadge.d.ts.map +1 -0
  22. package/dist/default/RatingBadge.js +23 -0
  23. package/dist/default/RatingBadge.js.map +1 -0
  24. package/dist/default/ReviewFormCard.d.ts +25 -0
  25. package/dist/default/ReviewFormCard.d.ts.map +1 -0
  26. package/dist/default/ReviewFormCard.js +42 -0
  27. package/dist/default/ReviewFormCard.js.map +1 -0
  28. package/dist/default/ReviewListPanel.d.ts +29 -0
  29. package/dist/default/ReviewListPanel.d.ts.map +1 -0
  30. package/dist/default/ReviewListPanel.js +47 -0
  31. package/dist/default/ReviewListPanel.js.map +1 -0
  32. package/dist/default/ReviewsPanel.d.ts +33 -0
  33. package/dist/default/ReviewsPanel.d.ts.map +1 -0
  34. package/dist/default/ReviewsPanel.js +17 -0
  35. package/dist/default/ReviewsPanel.js.map +1 -0
  36. package/dist/default/index.d.ts +31 -0
  37. package/dist/default/index.d.ts.map +1 -0
  38. package/dist/default/index.js +25 -0
  39. package/dist/default/index.js.map +1 -0
  40. package/dist/default/theme.d.ts +24 -0
  41. package/dist/default/theme.d.ts.map +1 -0
  42. package/dist/default/theme.js +39 -0
  43. package/dist/default/theme.js.map +1 -0
  44. package/dist/default/types.d.ts +12 -0
  45. package/dist/default/types.d.ts.map +1 -0
  46. package/dist/default/types.js +2 -0
  47. package/dist/default/types.js.map +1 -0
  48. package/dist/headless/ReviewAggregate.d.ts +63 -0
  49. package/dist/headless/ReviewAggregate.d.ts.map +1 -0
  50. package/dist/headless/ReviewAggregate.js +55 -0
  51. package/dist/headless/ReviewAggregate.js.map +1 -0
  52. package/dist/headless/ReviewForm.d.ts +83 -0
  53. package/dist/headless/ReviewForm.d.ts.map +1 -0
  54. package/dist/headless/ReviewForm.js +75 -0
  55. package/dist/headless/ReviewForm.js.map +1 -0
  56. package/dist/headless/ReviewList.d.ts +47 -0
  57. package/dist/headless/ReviewList.d.ts.map +1 -0
  58. package/dist/headless/ReviewList.js +51 -0
  59. package/dist/headless/ReviewList.js.map +1 -0
  60. package/dist/headless/ReviewsProvider.d.ts +19 -0
  61. package/dist/headless/ReviewsProvider.d.ts.map +1 -0
  62. package/dist/headless/ReviewsProvider.js +15 -0
  63. package/dist/headless/ReviewsProvider.js.map +1 -0
  64. package/dist/i18n/errorsMap.d.ts +12 -0
  65. package/dist/i18n/errorsMap.d.ts.map +1 -0
  66. package/dist/i18n/errorsMap.js +22 -0
  67. package/dist/i18n/errorsMap.js.map +1 -0
  68. package/dist/i18n/es.d.ts +19 -0
  69. package/dist/i18n/es.d.ts.map +1 -0
  70. package/dist/i18n/es.js +66 -0
  71. package/dist/i18n/es.js.map +1 -0
  72. package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
  73. package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
  74. package/dist/i18n/generated/errors.es.gen.js +58 -0
  75. package/dist/i18n/generated/errors.es.gen.js.map +1 -0
  76. package/dist/i18n/generated/errors.gen.d.ts +341 -0
  77. package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
  78. package/dist/i18n/generated/errors.gen.js +174 -0
  79. package/dist/i18n/generated/errors.gen.js.map +1 -0
  80. package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
  81. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
  82. package/dist/i18n/generated/errors.ru.gen.js +58 -0
  83. package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
  84. package/dist/i18n/keys.d.ts +59 -0
  85. package/dist/i18n/keys.d.ts.map +1 -0
  86. package/dist/i18n/keys.js +115 -0
  87. package/dist/i18n/keys.js.map +1 -0
  88. package/dist/i18n/ru.d.ts +22 -0
  89. package/dist/i18n/ru.d.ts.map +1 -0
  90. package/dist/i18n/ru.js +70 -0
  91. package/dist/i18n/ru.js.map +1 -0
  92. package/dist/index.d.ts +98 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +92 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/model/context.d.ts +11 -0
  97. package/dist/model/context.d.ts.map +1 -0
  98. package/dist/model/context.js +16 -0
  99. package/dist/model/context.js.map +1 -0
  100. package/dist/model/list.d.ts +52 -0
  101. package/dist/model/list.d.ts.map +1 -0
  102. package/dist/model/list.js +60 -0
  103. package/dist/model/list.js.map +1 -0
  104. package/dist/model/mutations.d.ts +32 -0
  105. package/dist/model/mutations.d.ts.map +1 -0
  106. package/dist/model/mutations.js +44 -0
  107. package/dist/model/mutations.js.map +1 -0
  108. package/dist/model/queries.d.ts +65 -0
  109. package/dist/model/queries.d.ts.map +1 -0
  110. package/dist/model/queries.js +80 -0
  111. package/dist/model/queries.js.map +1 -0
  112. package/dist/model/queryKeys.d.ts +27 -0
  113. package/dist/model/queryKeys.d.ts.map +1 -0
  114. package/dist/model/queryKeys.js +14 -0
  115. package/dist/model/queryKeys.js.map +1 -0
  116. package/dist/model/rating.d.ts +57 -0
  117. package/dist/model/rating.d.ts.map +1 -0
  118. package/dist/model/rating.js +47 -0
  119. package/dist/model/rating.js.map +1 -0
  120. package/dist/model/refusals.d.ts +51 -0
  121. package/dist/model/refusals.d.ts.map +1 -0
  122. package/dist/model/refusals.js +103 -0
  123. package/dist/model/refusals.js.map +1 -0
  124. package/dist/model/runtime.d.ts +44 -0
  125. package/dist/model/runtime.d.ts.map +1 -0
  126. package/dist/model/runtime.js +15 -0
  127. package/dist/model/runtime.js.map +1 -0
  128. package/llms.txt +80 -0
  129. package/manifest.json +577 -0
  130. package/package.json +115 -0
  131. package/src/analytics/generated/events.json +7 -0
  132. package/src/api/generated/schema.ts +362 -0
  133. package/src/api/reviewsApi.ts +170 -0
  134. package/src/api/types.ts +140 -0
  135. package/src/default/ErrorAlert.tsx +46 -0
  136. package/src/default/RatingBadge.tsx +81 -0
  137. package/src/default/ReviewFormCard.tsx +139 -0
  138. package/src/default/ReviewListPanel.tsx +204 -0
  139. package/src/default/ReviewsPanel.tsx +74 -0
  140. package/src/default/index.ts +30 -0
  141. package/src/default/theme.tsx +66 -0
  142. package/src/default/types.ts +12 -0
  143. package/src/headless/ReviewAggregate.tsx +94 -0
  144. package/src/headless/ReviewForm.tsx +163 -0
  145. package/src/headless/ReviewList.tsx +99 -0
  146. package/src/headless/ReviewsProvider.tsx +20 -0
  147. package/src/i18n/errorsMap.ts +33 -0
  148. package/src/i18n/es.ts +81 -0
  149. package/src/i18n/generated/errors.es.gen.ts +64 -0
  150. package/src/i18n/generated/errors.gen.ts +205 -0
  151. package/src/i18n/generated/errors.json +387 -0
  152. package/src/i18n/generated/errors.ru.gen.ts +64 -0
  153. package/src/i18n/keys.ts +133 -0
  154. package/src/i18n/ru.ts +85 -0
  155. package/src/index.ts +161 -0
  156. package/src/model/context.tsx +28 -0
  157. package/src/model/list.ts +74 -0
  158. package/src/model/mutations.ts +62 -0
  159. package/src/model/queries.ts +122 -0
  160. package/src/model/queryKeys.ts +45 -0
  161. package/src/model/rating.ts +101 -0
  162. package/src/model/refusals.ts +114 -0
  163. package/src/model/runtime.ts +61 -0
  164. 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
+ }