@btst/stack 2.7.0 → 2.8.1

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 (181) hide show
  1. package/README.md +1 -0
  2. package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.cjs +13 -0
  3. package/dist/packages/stack/src/plugins/blog/client/components/loading/post-navigation-skeleton.mjs +11 -0
  4. package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.cjs +17 -0
  5. package/dist/packages/stack/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.mjs +15 -0
  6. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.cjs +18 -7
  7. package/dist/packages/stack/src/plugins/blog/client/components/pages/post-page.internal.mjs +18 -7
  8. package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.cjs +48 -52
  9. package/dist/packages/stack/src/plugins/blog/client/components/shared/post-navigation.mjs +49 -53
  10. package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.cjs +34 -37
  11. package/dist/packages/stack/src/plugins/blog/client/components/shared/recent-posts-carousel.mjs +35 -38
  12. package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.cjs +4 -21
  13. package/dist/packages/stack/src/plugins/blog/client/hooks/blog-hooks.mjs +4 -21
  14. package/dist/packages/stack/src/plugins/comments/api/getters.cjs +284 -0
  15. package/dist/packages/stack/src/plugins/comments/api/getters.mjs +280 -0
  16. package/dist/packages/stack/src/plugins/comments/api/mutations.cjs +118 -0
  17. package/dist/packages/stack/src/plugins/comments/api/mutations.mjs +112 -0
  18. package/dist/packages/stack/src/plugins/comments/api/plugin.cjs +335 -0
  19. package/dist/packages/stack/src/plugins/comments/api/plugin.mjs +333 -0
  20. package/dist/packages/stack/src/plugins/comments/api/query-key-defs.cjs +60 -0
  21. package/dist/packages/stack/src/plugins/comments/api/query-key-defs.mjs +55 -0
  22. package/dist/packages/stack/src/plugins/comments/api/serializers.cjs +23 -0
  23. package/dist/packages/stack/src/plugins/comments/api/serializers.mjs +21 -0
  24. package/dist/packages/stack/src/plugins/comments/client/components/comment-count.cjs +46 -0
  25. package/dist/packages/stack/src/plugins/comments/client/components/comment-count.mjs +44 -0
  26. package/dist/packages/stack/src/plugins/comments/client/components/comment-form.cjs +86 -0
  27. package/dist/packages/stack/src/plugins/comments/client/components/comment-form.mjs +84 -0
  28. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +540 -0
  29. package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +538 -0
  30. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.cjs +64 -0
  31. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.cjs +426 -0
  32. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.internal.mjs +424 -0
  33. package/dist/packages/stack/src/plugins/comments/client/components/pages/moderation-page.mjs +62 -0
  34. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.cjs +66 -0
  35. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.cjs +256 -0
  36. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.internal.mjs +254 -0
  37. package/dist/packages/stack/src/plugins/comments/client/components/pages/my-comments-page.mjs +64 -0
  38. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.cjs +86 -0
  39. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.cjs +191 -0
  40. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.internal.mjs +189 -0
  41. package/dist/packages/stack/src/plugins/comments/client/components/pages/resource-comments-page.mjs +84 -0
  42. package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.cjs +27 -0
  43. package/dist/packages/stack/src/plugins/comments/client/components/shared/page-wrapper.mjs +25 -0
  44. package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.cjs +37 -0
  45. package/dist/packages/stack/src/plugins/comments/client/components/shared/pagination.mjs +35 -0
  46. package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.cjs +476 -0
  47. package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.mjs +464 -0
  48. package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.cjs +67 -0
  49. package/dist/packages/stack/src/plugins/comments/client/localization/comments-moderation.mjs +65 -0
  50. package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.cjs +27 -0
  51. package/dist/packages/stack/src/plugins/comments/client/localization/comments-my.mjs +25 -0
  52. package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.cjs +30 -0
  53. package/dist/packages/stack/src/plugins/comments/client/localization/comments-thread.mjs +28 -0
  54. package/dist/packages/stack/src/plugins/comments/client/localization/index.cjs +13 -0
  55. package/dist/packages/stack/src/plugins/comments/client/localization/index.mjs +11 -0
  56. package/dist/packages/stack/src/plugins/comments/client/plugin.cjs +116 -0
  57. package/dist/packages/stack/src/plugins/comments/client/plugin.mjs +114 -0
  58. package/dist/packages/stack/src/plugins/comments/client/utils.cjs +41 -0
  59. package/dist/packages/stack/src/plugins/comments/client/utils.mjs +37 -0
  60. package/dist/packages/stack/src/plugins/comments/db.cjs +75 -0
  61. package/dist/packages/stack/src/plugins/comments/db.mjs +73 -0
  62. package/dist/packages/stack/src/plugins/comments/schemas.cjs +45 -0
  63. package/dist/packages/stack/src/plugins/comments/schemas.mjs +38 -0
  64. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.cjs +0 -1
  65. package/dist/packages/stack/src/plugins/kanban/client/components/forms/task-form.mjs +0 -1
  66. package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.cjs +39 -22
  67. package/dist/packages/stack/src/plugins/kanban/client/components/pages/board-page.internal.mjs +40 -23
  68. package/dist/packages/ui/src/components/avatar.mjs +1 -1
  69. package/dist/packages/ui/src/components/pagination-controls.cjs +64 -0
  70. package/dist/packages/ui/src/components/pagination-controls.mjs +62 -0
  71. package/dist/packages/ui/src/components/when-visible.cjs +39 -0
  72. package/dist/packages/ui/src/components/when-visible.mjs +37 -0
  73. package/dist/plugins/blog/client/hooks/index.d.cts +1 -1
  74. package/dist/plugins/blog/client/hooks/index.d.mts +1 -1
  75. package/dist/plugins/blog/client/hooks/index.d.ts +1 -1
  76. package/dist/plugins/blog/client/index.d.cts +24 -2
  77. package/dist/plugins/blog/client/index.d.mts +24 -2
  78. package/dist/plugins/blog/client/index.d.ts +24 -2
  79. package/dist/plugins/comments/api/index.cjs +21 -0
  80. package/dist/plugins/comments/api/index.d.cts +126 -0
  81. package/dist/plugins/comments/api/index.d.mts +126 -0
  82. package/dist/plugins/comments/api/index.d.ts +126 -0
  83. package/dist/plugins/comments/api/index.mjs +5 -0
  84. package/dist/plugins/comments/client/components/index.cjs +15 -0
  85. package/dist/plugins/comments/client/components/index.d.cts +125 -0
  86. package/dist/plugins/comments/client/components/index.d.mts +125 -0
  87. package/dist/plugins/comments/client/components/index.d.ts +125 -0
  88. package/dist/plugins/comments/client/components/index.mjs +5 -0
  89. package/dist/plugins/comments/client/hooks/index.cjs +17 -0
  90. package/dist/plugins/comments/client/hooks/index.d.cts +200 -0
  91. package/dist/plugins/comments/client/hooks/index.d.mts +200 -0
  92. package/dist/plugins/comments/client/hooks/index.d.ts +200 -0
  93. package/dist/plugins/comments/client/hooks/index.mjs +1 -0
  94. package/dist/plugins/comments/client/index.cjs +9 -0
  95. package/dist/plugins/comments/client/index.d.cts +262 -0
  96. package/dist/plugins/comments/client/index.d.mts +262 -0
  97. package/dist/plugins/comments/client/index.d.ts +262 -0
  98. package/dist/plugins/comments/client/index.mjs +2 -0
  99. package/dist/plugins/comments/client.css +2 -0
  100. package/dist/plugins/comments/query-keys.cjs +113 -0
  101. package/dist/plugins/comments/query-keys.d.cts +71 -0
  102. package/dist/plugins/comments/query-keys.d.mts +71 -0
  103. package/dist/plugins/comments/query-keys.d.ts +71 -0
  104. package/dist/plugins/comments/query-keys.mjs +111 -0
  105. package/dist/plugins/comments/style.css +15 -0
  106. package/dist/plugins/kanban/api/index.d.cts +1 -1
  107. package/dist/plugins/kanban/api/index.d.mts +1 -1
  108. package/dist/plugins/kanban/api/index.d.ts +1 -1
  109. package/dist/plugins/kanban/client/hooks/index.d.cts +1 -1
  110. package/dist/plugins/kanban/client/hooks/index.d.mts +1 -1
  111. package/dist/plugins/kanban/client/hooks/index.d.ts +1 -1
  112. package/dist/plugins/kanban/client/index.d.cts +1 -1
  113. package/dist/plugins/kanban/client/index.d.mts +1 -1
  114. package/dist/plugins/kanban/client/index.d.ts +1 -1
  115. package/dist/plugins/kanban/query-keys.d.cts +1 -1
  116. package/dist/plugins/kanban/query-keys.d.mts +1 -1
  117. package/dist/plugins/kanban/query-keys.d.ts +1 -1
  118. package/dist/shared/{stack.FeaWkglm.d.ts → stack.BxFl46lB.d.cts} +24 -1
  119. package/dist/shared/stack.C-b3Sn8j.d.cts +142 -0
  120. package/dist/shared/stack.C-b3Sn8j.d.mts +142 -0
  121. package/dist/shared/stack.C-b3Sn8j.d.ts +142 -0
  122. package/dist/shared/stack.CJE9sAjV.d.ts +335 -0
  123. package/dist/shared/stack.CmHRdhl8.d.cts +335 -0
  124. package/dist/shared/{stack.CNLHlv7r.d.mts → stack.DOZ1EXjM.d.mts} +6 -12
  125. package/dist/shared/{stack.FeaWkglm.d.mts → stack.DRpeDS6X.d.ts} +24 -1
  126. package/dist/shared/{stack.CQAZwXhV.d.cts → stack.DX-tQ93o.d.cts} +6 -12
  127. package/dist/shared/stack.Dcz6636A.d.mts +335 -0
  128. package/dist/shared/{stack.FeaWkglm.d.cts → stack.Jb0kQDJC.d.mts} +24 -1
  129. package/dist/shared/stack.Ldfkr5b2.d.cts +112 -0
  130. package/dist/shared/stack.Ldfkr5b2.d.mts +112 -0
  131. package/dist/shared/stack.Ldfkr5b2.d.ts +112 -0
  132. package/dist/shared/{stack.D3BsrpAz.d.ts → stack.VF6FhyZw.d.ts} +6 -12
  133. package/package.json +69 -4
  134. package/src/plugins/blog/client/components/loading/post-navigation-skeleton.tsx +10 -0
  135. package/src/plugins/blog/client/components/loading/recent-posts-carousel-skeleton.tsx +18 -0
  136. package/src/plugins/blog/client/components/pages/post-page.internal.tsx +23 -8
  137. package/src/plugins/blog/client/components/shared/post-navigation.tsx +0 -5
  138. package/src/plugins/blog/client/components/shared/recent-posts-carousel.tsx +1 -5
  139. package/src/plugins/blog/client/hooks/blog-hooks.tsx +8 -33
  140. package/src/plugins/blog/client/overrides.ts +26 -1
  141. package/src/plugins/cms/client/components/shared/pagination.tsx +14 -42
  142. package/src/plugins/comments/api/getters.ts +444 -0
  143. package/src/plugins/comments/api/index.ts +21 -0
  144. package/src/plugins/comments/api/mutations.ts +206 -0
  145. package/src/plugins/comments/api/plugin.ts +628 -0
  146. package/src/plugins/comments/api/query-key-defs.ts +143 -0
  147. package/src/plugins/comments/api/serializers.ts +37 -0
  148. package/src/plugins/comments/client/components/comment-count.tsx +66 -0
  149. package/src/plugins/comments/client/components/comment-form.tsx +112 -0
  150. package/src/plugins/comments/client/components/comment-thread.tsx +799 -0
  151. package/src/plugins/comments/client/components/index.tsx +11 -0
  152. package/src/plugins/comments/client/components/pages/moderation-page.internal.tsx +550 -0
  153. package/src/plugins/comments/client/components/pages/moderation-page.tsx +70 -0
  154. package/src/plugins/comments/client/components/pages/my-comments-page.internal.tsx +367 -0
  155. package/src/plugins/comments/client/components/pages/my-comments-page.tsx +72 -0
  156. package/src/plugins/comments/client/components/pages/resource-comments-page.internal.tsx +225 -0
  157. package/src/plugins/comments/client/components/pages/resource-comments-page.tsx +97 -0
  158. package/src/plugins/comments/client/components/shared/page-wrapper.tsx +32 -0
  159. package/src/plugins/comments/client/components/shared/pagination.tsx +44 -0
  160. package/src/plugins/comments/client/hooks/index.tsx +13 -0
  161. package/src/plugins/comments/client/hooks/use-comments.tsx +717 -0
  162. package/src/plugins/comments/client/index.ts +14 -0
  163. package/src/plugins/comments/client/localization/comments-moderation.ts +75 -0
  164. package/src/plugins/comments/client/localization/comments-my.ts +32 -0
  165. package/src/plugins/comments/client/localization/comments-thread.ts +32 -0
  166. package/src/plugins/comments/client/localization/index.ts +11 -0
  167. package/src/plugins/comments/client/overrides.ts +164 -0
  168. package/src/plugins/comments/client/plugin.tsx +195 -0
  169. package/src/plugins/comments/client/utils.ts +67 -0
  170. package/src/plugins/comments/client.css +2 -0
  171. package/src/plugins/comments/db.ts +77 -0
  172. package/src/plugins/comments/query-keys.ts +189 -0
  173. package/src/plugins/comments/schemas.ts +72 -0
  174. package/src/plugins/comments/style.css +15 -0
  175. package/src/plugins/comments/types.ts +73 -0
  176. package/src/plugins/kanban/client/components/forms/task-form.tsx +0 -1
  177. package/src/plugins/kanban/client/components/pages/board-page.internal.tsx +46 -27
  178. package/src/plugins/kanban/client/overrides.ts +27 -1
  179. package/dist/shared/{stack.Rtcvl8sS.d.cts → stack.BOokfhZD.d.cts} +3 -3
  180. package/dist/shared/{stack.D4Cea8II.d.ts → stack.BvCR4-9H.d.ts} +3 -3
  181. package/dist/shared/{stack.HE_IvqV5.d.mts → stack.CWxAl9K3.d.mts} +3 -3
@@ -0,0 +1,333 @@
1
+ import { defineBackendPlugin, createEndpoint } from '@btst/stack/plugins/api';
2
+ import { z } from 'zod';
3
+ import { commentsSchema } from '../db.mjs';
4
+ import { CommentListQuerySchema, createCommentSchema, updateCommentSchema, CommentCountQuerySchema, updateCommentStatusSchema } from '../schemas.mjs';
5
+ import { getCommentCount, getCommentById, listComments } from './getters.mjs';
6
+ import { createComment, updateComment, toggleCommentLike, updateCommentStatus, deleteComment } from './mutations.mjs';
7
+ import { runHookWithShim } from '../../utils.mjs';
8
+
9
+ const commentsBackendPlugin = (options) => {
10
+ const postingEnabled = options.allowPosting !== false;
11
+ const editingEnabled = options.allowEditing !== false;
12
+ const onBeforePost = options.allowPosting !== false ? options.onBeforePost : void 0;
13
+ const resolveCurrentUserId = options.allowPosting !== false ? options.resolveCurrentUserId : void 0;
14
+ return defineBackendPlugin({
15
+ name: "comments",
16
+ dbPlugin: commentsSchema,
17
+ api: (adapter) => ({
18
+ listComments: (params) => listComments(adapter, params, options?.resolveUser),
19
+ getCommentById: (id, currentUserId) => getCommentById(adapter, id, options?.resolveUser, currentUserId),
20
+ getCommentCount: (params) => getCommentCount(adapter, params)
21
+ }),
22
+ routes: (adapter) => {
23
+ const listCommentsEndpoint = createEndpoint(
24
+ "/comments",
25
+ {
26
+ method: "GET",
27
+ query: CommentListQuerySchema
28
+ },
29
+ async (ctx) => {
30
+ const context = {
31
+ query: ctx.query,
32
+ request: ctx.request,
33
+ headers: ctx.headers
34
+ };
35
+ if (ctx.query.authorId) {
36
+ if (!options?.onBeforeListByAuthor) {
37
+ throw ctx.error(403, {
38
+ message: "Forbidden: authorId filter requires onBeforeListByAuthor hook"
39
+ });
40
+ }
41
+ await runHookWithShim(
42
+ () => options.onBeforeListByAuthor(
43
+ ctx.query.authorId,
44
+ ctx.query,
45
+ context
46
+ ),
47
+ ctx.error,
48
+ "Forbidden: Cannot list comments for this author"
49
+ );
50
+ }
51
+ if (ctx.query.status && ctx.query.status !== "approved") {
52
+ if (!options?.onBeforeList) {
53
+ throw ctx.error(403, {
54
+ message: "Forbidden: status filter requires authorization"
55
+ });
56
+ }
57
+ await runHookWithShim(
58
+ () => options.onBeforeList(ctx.query, context),
59
+ ctx.error,
60
+ "Forbidden: Cannot list comments with this status filter"
61
+ );
62
+ } else if (options?.onBeforeList && !ctx.query.authorId) {
63
+ await runHookWithShim(
64
+ () => options.onBeforeList(ctx.query, context),
65
+ ctx.error,
66
+ "Forbidden: Cannot list comments"
67
+ );
68
+ }
69
+ let resolvedCurrentUserId;
70
+ if (resolveCurrentUserId) {
71
+ try {
72
+ const result = await resolveCurrentUserId(context);
73
+ resolvedCurrentUserId = result ?? void 0;
74
+ } catch {
75
+ resolvedCurrentUserId = void 0;
76
+ }
77
+ }
78
+ return await listComments(
79
+ adapter,
80
+ { ...ctx.query, currentUserId: resolvedCurrentUserId },
81
+ options?.resolveUser
82
+ );
83
+ }
84
+ );
85
+ const createCommentEndpoint = createEndpoint(
86
+ "/comments",
87
+ {
88
+ method: "POST",
89
+ body: createCommentSchema
90
+ },
91
+ async (ctx) => {
92
+ if (!postingEnabled) {
93
+ throw ctx.error(403, { message: "Posting comments is disabled" });
94
+ }
95
+ const context = {
96
+ body: ctx.body,
97
+ headers: ctx.headers
98
+ };
99
+ const { authorId } = await runHookWithShim(
100
+ () => onBeforePost(ctx.body, context),
101
+ ctx.error,
102
+ "Unauthorized: Cannot post comment"
103
+ );
104
+ const status = options?.autoApprove ? "approved" : "pending";
105
+ const comment = await createComment(adapter, {
106
+ ...ctx.body,
107
+ authorId,
108
+ status
109
+ });
110
+ if (options?.onAfterPost) {
111
+ await options.onAfterPost(comment, context);
112
+ }
113
+ const serialized = await getCommentById(
114
+ adapter,
115
+ comment.id,
116
+ options?.resolveUser
117
+ );
118
+ if (!serialized) {
119
+ throw ctx.error(500, {
120
+ message: "Failed to retrieve created comment"
121
+ });
122
+ }
123
+ return serialized;
124
+ }
125
+ );
126
+ const updateCommentEndpoint = createEndpoint(
127
+ "/comments/:id",
128
+ {
129
+ method: "PATCH",
130
+ body: updateCommentSchema
131
+ },
132
+ async (ctx) => {
133
+ if (!editingEnabled) {
134
+ throw ctx.error(403, { message: "Editing comments is disabled" });
135
+ }
136
+ const { id } = ctx.params;
137
+ const context = {
138
+ params: ctx.params,
139
+ body: ctx.body,
140
+ headers: ctx.headers
141
+ };
142
+ if (!options?.onBeforeEdit) {
143
+ throw ctx.error(403, {
144
+ message: "Forbidden: editing comments requires the onBeforeEdit hook"
145
+ });
146
+ }
147
+ await runHookWithShim(
148
+ () => options.onBeforeEdit(id, { body: ctx.body.body }, context),
149
+ ctx.error,
150
+ "Unauthorized: Cannot edit comment"
151
+ );
152
+ const updated = await updateComment(adapter, id, ctx.body.body);
153
+ if (!updated) {
154
+ throw ctx.error(404, { message: "Comment not found" });
155
+ }
156
+ if (options?.onAfterEdit) {
157
+ await options.onAfterEdit(updated, context);
158
+ }
159
+ const serialized = await getCommentById(
160
+ adapter,
161
+ updated.id,
162
+ options?.resolveUser
163
+ );
164
+ if (!serialized) {
165
+ throw ctx.error(500, {
166
+ message: "Failed to retrieve updated comment"
167
+ });
168
+ }
169
+ return serialized;
170
+ }
171
+ );
172
+ const getCommentCountEndpoint = createEndpoint(
173
+ "/comments/count",
174
+ {
175
+ method: "GET",
176
+ query: CommentCountQuerySchema
177
+ },
178
+ async (ctx) => {
179
+ const context = {
180
+ query: ctx.query,
181
+ headers: ctx.headers
182
+ };
183
+ if (ctx.query.status && ctx.query.status !== "approved") {
184
+ if (!options?.onBeforeList) {
185
+ throw ctx.error(403, {
186
+ message: "Forbidden: status filter requires authorization"
187
+ });
188
+ }
189
+ await runHookWithShim(
190
+ () => options.onBeforeList(
191
+ { ...ctx.query, status: ctx.query.status },
192
+ context
193
+ ),
194
+ ctx.error,
195
+ "Forbidden: Cannot count comments with this status filter"
196
+ );
197
+ } else if (options?.onBeforeList) {
198
+ await runHookWithShim(
199
+ () => options.onBeforeList(
200
+ { ...ctx.query, status: ctx.query.status },
201
+ context
202
+ ),
203
+ ctx.error,
204
+ "Forbidden: Cannot count comments"
205
+ );
206
+ }
207
+ const count = await getCommentCount(adapter, ctx.query);
208
+ return { count };
209
+ }
210
+ );
211
+ const toggleLikeEndpoint = createEndpoint(
212
+ "/comments/:id/like",
213
+ {
214
+ method: "POST",
215
+ body: z.object({ authorId: z.string().min(1) })
216
+ },
217
+ async (ctx) => {
218
+ const { id } = ctx.params;
219
+ const context = {
220
+ params: ctx.params,
221
+ body: ctx.body,
222
+ headers: ctx.headers
223
+ };
224
+ if (!options?.onBeforeLike) {
225
+ throw ctx.error(403, {
226
+ message: "Forbidden: toggling likes requires the onBeforeLike hook"
227
+ });
228
+ }
229
+ await runHookWithShim(
230
+ () => options.onBeforeLike(id, ctx.body.authorId, context),
231
+ ctx.error,
232
+ "Unauthorized: Cannot like comment"
233
+ );
234
+ const result = await toggleCommentLike(
235
+ adapter,
236
+ id,
237
+ ctx.body.authorId
238
+ );
239
+ return result;
240
+ }
241
+ );
242
+ const updateStatusEndpoint = createEndpoint(
243
+ "/comments/:id/status",
244
+ {
245
+ method: "PATCH",
246
+ body: updateCommentStatusSchema
247
+ },
248
+ async (ctx) => {
249
+ const { id } = ctx.params;
250
+ const context = {
251
+ params: ctx.params,
252
+ body: ctx.body,
253
+ headers: ctx.headers
254
+ };
255
+ if (!options?.onBeforeStatusChange) {
256
+ throw ctx.error(403, {
257
+ message: "Forbidden: changing comment status requires the onBeforeStatusChange hook"
258
+ });
259
+ }
260
+ await runHookWithShim(
261
+ () => options.onBeforeStatusChange(id, ctx.body.status, context),
262
+ ctx.error,
263
+ "Unauthorized: Cannot change comment status"
264
+ );
265
+ const updated = await updateCommentStatus(
266
+ adapter,
267
+ id,
268
+ ctx.body.status
269
+ );
270
+ if (!updated) {
271
+ throw ctx.error(404, { message: "Comment not found" });
272
+ }
273
+ if (ctx.body.status === "approved" && options?.onAfterApprove) {
274
+ await options.onAfterApprove(updated, context);
275
+ }
276
+ const serialized = await getCommentById(
277
+ adapter,
278
+ updated.id,
279
+ options?.resolveUser
280
+ );
281
+ if (!serialized) {
282
+ throw ctx.error(500, {
283
+ message: "Failed to retrieve updated comment"
284
+ });
285
+ }
286
+ return serialized;
287
+ }
288
+ );
289
+ const deleteCommentEndpoint = createEndpoint(
290
+ "/comments/:id",
291
+ {
292
+ method: "DELETE"
293
+ },
294
+ async (ctx) => {
295
+ const { id } = ctx.params;
296
+ const context = {
297
+ params: ctx.params,
298
+ headers: ctx.headers
299
+ };
300
+ if (!options?.onBeforeDelete) {
301
+ throw ctx.error(403, {
302
+ message: "Forbidden: deleting comments requires the onBeforeDelete hook"
303
+ });
304
+ }
305
+ await runHookWithShim(
306
+ () => options.onBeforeDelete(id, context),
307
+ ctx.error,
308
+ "Unauthorized: Cannot delete comment"
309
+ );
310
+ const deleted = await deleteComment(adapter, id);
311
+ if (!deleted) {
312
+ throw ctx.error(404, { message: "Comment not found" });
313
+ }
314
+ if (options?.onAfterDelete) {
315
+ await options.onAfterDelete(id, context);
316
+ }
317
+ return { success: true };
318
+ }
319
+ );
320
+ return {
321
+ listComments: listCommentsEndpoint,
322
+ ...postingEnabled && { createComment: createCommentEndpoint },
323
+ ...editingEnabled && { updateComment: updateCommentEndpoint },
324
+ getCommentCount: getCommentCountEndpoint,
325
+ toggleLike: toggleLikeEndpoint,
326
+ updateCommentStatus: updateStatusEndpoint,
327
+ deleteComment: deleteCommentEndpoint
328
+ };
329
+ }
330
+ });
331
+ };
332
+
333
+ export { commentsBackendPlugin };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ function commentsListDiscriminator(params) {
4
+ return {
5
+ resourceId: params?.resourceId,
6
+ resourceType: params?.resourceType,
7
+ parentId: params?.parentId,
8
+ status: params?.status,
9
+ currentUserId: params?.currentUserId,
10
+ authorId: params?.authorId,
11
+ sort: params?.sort,
12
+ limit: params?.limit ?? 20,
13
+ offset: params?.offset ?? 0
14
+ };
15
+ }
16
+ function commentCountDiscriminator(params) {
17
+ return {
18
+ resourceId: params.resourceId,
19
+ resourceType: params.resourceType,
20
+ status: params.status
21
+ };
22
+ }
23
+ function commentsThreadDiscriminator(params) {
24
+ return {
25
+ resourceId: params?.resourceId,
26
+ resourceType: params?.resourceType,
27
+ parentId: params?.parentId,
28
+ status: params?.status,
29
+ currentUserId: params?.currentUserId,
30
+ limit: params?.limit ?? 20
31
+ };
32
+ }
33
+ const COMMENTS_QUERY_KEYS = {
34
+ /**
35
+ * Key for comments list query.
36
+ * Full key: ["comments", "list", { resourceId, resourceType, parentId, status, currentUserId, limit, offset }]
37
+ */
38
+ commentsList: (params) => ["comments", "list", commentsListDiscriminator(params)],
39
+ /**
40
+ * Key for a single comment detail query.
41
+ * Full key: ["comments", "detail", id]
42
+ */
43
+ commentDetail: (id) => ["comments", "detail", id],
44
+ /**
45
+ * Key for comment count query.
46
+ * Full key: ["comments", "count", { resourceId, resourceType, status }]
47
+ */
48
+ commentCount: (params) => ["comments", "count", commentCountDiscriminator(params)],
49
+ /**
50
+ * Key for the infinite thread query (top-level comments, load-more).
51
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
52
+ * Offset is excluded — it is driven by `pageParam`, not baked into the key.
53
+ */
54
+ commentsThread: (params) => ["commentsThread", "list", commentsThreadDiscriminator(params)]
55
+ };
56
+
57
+ exports.COMMENTS_QUERY_KEYS = COMMENTS_QUERY_KEYS;
58
+ exports.commentCountDiscriminator = commentCountDiscriminator;
59
+ exports.commentsListDiscriminator = commentsListDiscriminator;
60
+ exports.commentsThreadDiscriminator = commentsThreadDiscriminator;
@@ -0,0 +1,55 @@
1
+ function commentsListDiscriminator(params) {
2
+ return {
3
+ resourceId: params?.resourceId,
4
+ resourceType: params?.resourceType,
5
+ parentId: params?.parentId,
6
+ status: params?.status,
7
+ currentUserId: params?.currentUserId,
8
+ authorId: params?.authorId,
9
+ sort: params?.sort,
10
+ limit: params?.limit ?? 20,
11
+ offset: params?.offset ?? 0
12
+ };
13
+ }
14
+ function commentCountDiscriminator(params) {
15
+ return {
16
+ resourceId: params.resourceId,
17
+ resourceType: params.resourceType,
18
+ status: params.status
19
+ };
20
+ }
21
+ function commentsThreadDiscriminator(params) {
22
+ return {
23
+ resourceId: params?.resourceId,
24
+ resourceType: params?.resourceType,
25
+ parentId: params?.parentId,
26
+ status: params?.status,
27
+ currentUserId: params?.currentUserId,
28
+ limit: params?.limit ?? 20
29
+ };
30
+ }
31
+ const COMMENTS_QUERY_KEYS = {
32
+ /**
33
+ * Key for comments list query.
34
+ * Full key: ["comments", "list", { resourceId, resourceType, parentId, status, currentUserId, limit, offset }]
35
+ */
36
+ commentsList: (params) => ["comments", "list", commentsListDiscriminator(params)],
37
+ /**
38
+ * Key for a single comment detail query.
39
+ * Full key: ["comments", "detail", id]
40
+ */
41
+ commentDetail: (id) => ["comments", "detail", id],
42
+ /**
43
+ * Key for comment count query.
44
+ * Full key: ["comments", "count", { resourceId, resourceType, status }]
45
+ */
46
+ commentCount: (params) => ["comments", "count", commentCountDiscriminator(params)],
47
+ /**
48
+ * Key for the infinite thread query (top-level comments, load-more).
49
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
50
+ * Offset is excluded — it is driven by `pageParam`, not baked into the key.
51
+ */
52
+ commentsThread: (params) => ["commentsThread", "list", commentsThreadDiscriminator(params)]
53
+ };
54
+
55
+ export { COMMENTS_QUERY_KEYS, commentCountDiscriminator, commentsListDiscriminator, commentsThreadDiscriminator };
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ function serializeComment(comment) {
4
+ return {
5
+ id: comment.id,
6
+ resourceId: comment.resourceId,
7
+ resourceType: comment.resourceType,
8
+ parentId: comment.parentId ?? null,
9
+ authorId: comment.authorId,
10
+ resolvedAuthorName: "[deleted]",
11
+ resolvedAvatarUrl: null,
12
+ isLikedByCurrentUser: false,
13
+ body: comment.body,
14
+ status: comment.status,
15
+ likes: comment.likes,
16
+ editedAt: comment.editedAt?.toISOString() ?? null,
17
+ createdAt: comment.createdAt.toISOString(),
18
+ updatedAt: comment.updatedAt.toISOString(),
19
+ replyCount: 0
20
+ };
21
+ }
22
+
23
+ exports.serializeComment = serializeComment;
@@ -0,0 +1,21 @@
1
+ function serializeComment(comment) {
2
+ return {
3
+ id: comment.id,
4
+ resourceId: comment.resourceId,
5
+ resourceType: comment.resourceType,
6
+ parentId: comment.parentId ?? null,
7
+ authorId: comment.authorId,
8
+ resolvedAuthorName: "[deleted]",
9
+ resolvedAvatarUrl: null,
10
+ isLikedByCurrentUser: false,
11
+ body: comment.body,
12
+ status: comment.status,
13
+ likes: comment.likes,
14
+ editedAt: comment.editedAt?.toISOString() ?? null,
15
+ createdAt: comment.createdAt.toISOString(),
16
+ updatedAt: comment.updatedAt.toISOString(),
17
+ replyCount: 0
18
+ };
19
+ }
20
+
21
+ export { serializeComment };
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ const jsxRuntime = require('react/jsx-runtime');
5
+ const LucideIcons = require('lucide-react');
6
+ const useComments = require('../hooks/use-comments.cjs');
7
+
8
+ function CommentCount({
9
+ resourceId,
10
+ resourceType,
11
+ status = "approved",
12
+ apiBaseURL,
13
+ apiBasePath,
14
+ headers,
15
+ className
16
+ }) {
17
+ const { count, isLoading } = useComments.useCommentCount(
18
+ { apiBaseURL, apiBasePath, headers },
19
+ { resourceId, resourceType, status }
20
+ );
21
+ if (isLoading) {
22
+ return /* @__PURE__ */ jsxRuntime.jsxs(
23
+ "span",
24
+ {
25
+ className: `inline-flex items-center gap-1 text-xs text-muted-foreground ${className ?? ""}`,
26
+ children: [
27
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.MessageSquare, { className: "h-3.5 w-3.5" }),
28
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "animate-pulse", children: "\u2026" })
29
+ ]
30
+ }
31
+ );
32
+ }
33
+ return /* @__PURE__ */ jsxRuntime.jsxs(
34
+ "span",
35
+ {
36
+ className: `inline-flex items-center gap-1 text-xs text-muted-foreground ${className ?? ""}`,
37
+ "data-testid": "comment-count",
38
+ children: [
39
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.MessageSquare, { className: "h-3.5 w-3.5" }),
40
+ count
41
+ ]
42
+ }
43
+ );
44
+ }
45
+
46
+ exports.CommentCount = CommentCount;
@@ -0,0 +1,44 @@
1
+ "use client";
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { MessageSquare } from 'lucide-react';
4
+ import { useCommentCount } from '../hooks/use-comments.mjs';
5
+
6
+ function CommentCount({
7
+ resourceId,
8
+ resourceType,
9
+ status = "approved",
10
+ apiBaseURL,
11
+ apiBasePath,
12
+ headers,
13
+ className
14
+ }) {
15
+ const { count, isLoading } = useCommentCount(
16
+ { apiBaseURL, apiBasePath, headers },
17
+ { resourceId, resourceType, status }
18
+ );
19
+ if (isLoading) {
20
+ return /* @__PURE__ */ jsxs(
21
+ "span",
22
+ {
23
+ className: `inline-flex items-center gap-1 text-xs text-muted-foreground ${className ?? ""}`,
24
+ children: [
25
+ /* @__PURE__ */ jsx(MessageSquare, { className: "h-3.5 w-3.5" }),
26
+ /* @__PURE__ */ jsx("span", { className: "animate-pulse", children: "\u2026" })
27
+ ]
28
+ }
29
+ );
30
+ }
31
+ return /* @__PURE__ */ jsxs(
32
+ "span",
33
+ {
34
+ className: `inline-flex items-center gap-1 text-xs text-muted-foreground ${className ?? ""}`,
35
+ "data-testid": "comment-count",
36
+ children: [
37
+ /* @__PURE__ */ jsx(MessageSquare, { className: "h-3.5 w-3.5" }),
38
+ count
39
+ ]
40
+ }
41
+ );
42
+ }
43
+
44
+ export { CommentCount };
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ const jsxRuntime = require('react/jsx-runtime');
5
+ const React = require('react');
6
+ const button = require('../../../../../../ui/src/components/button.cjs');
7
+ const textarea = require('../../../../../../ui/src/components/textarea.cjs');
8
+ const index = require('../localization/index.cjs');
9
+
10
+ function CommentForm({
11
+ authorId: _authorId,
12
+ initialBody = "",
13
+ submitLabel,
14
+ onSubmit,
15
+ onCancel,
16
+ InputComponent,
17
+ localization: localizationProp
18
+ }) {
19
+ const loc = { ...index.COMMENTS_LOCALIZATION, ...localizationProp };
20
+ const [body, setBody] = React.useState(initialBody);
21
+ const [isPending, setIsPending] = React.useState(false);
22
+ const [error, setError] = React.useState(null);
23
+ const resolvedSubmitLabel = submitLabel ?? loc.COMMENTS_FORM_POST_COMMENT;
24
+ const handleSubmit = async (e) => {
25
+ e.preventDefault();
26
+ if (!body.trim()) return;
27
+ setError(null);
28
+ setIsPending(true);
29
+ try {
30
+ await onSubmit(body.trim());
31
+ setBody("");
32
+ } catch (err) {
33
+ setError(
34
+ err instanceof Error ? err.message : loc.COMMENTS_FORM_SUBMIT_ERROR
35
+ );
36
+ } finally {
37
+ setIsPending(false);
38
+ }
39
+ };
40
+ return /* @__PURE__ */ jsxRuntime.jsxs(
41
+ "form",
42
+ {
43
+ "data-testid": "comment-form",
44
+ onSubmit: handleSubmit,
45
+ className: "flex flex-col gap-2",
46
+ children: [
47
+ InputComponent ? /* @__PURE__ */ jsxRuntime.jsx(
48
+ InputComponent,
49
+ {
50
+ value: body,
51
+ onChange: setBody,
52
+ disabled: isPending,
53
+ placeholder: loc.COMMENTS_FORM_PLACEHOLDER
54
+ }
55
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
56
+ textarea.Textarea,
57
+ {
58
+ value: body,
59
+ onChange: (e) => setBody(e.target.value),
60
+ placeholder: loc.COMMENTS_FORM_PLACEHOLDER,
61
+ disabled: isPending,
62
+ rows: 3,
63
+ className: "resize-none"
64
+ }
65
+ ),
66
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: error }),
67
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 justify-end", children: [
68
+ onCancel && /* @__PURE__ */ jsxRuntime.jsx(
69
+ button.Button,
70
+ {
71
+ type: "button",
72
+ variant: "ghost",
73
+ size: "sm",
74
+ onClick: onCancel,
75
+ disabled: isPending,
76
+ children: loc.COMMENTS_FORM_CANCEL
77
+ }
78
+ ),
79
+ /* @__PURE__ */ jsxRuntime.jsx(button.Button, { type: "submit", size: "sm", disabled: isPending || !body.trim(), children: isPending ? loc.COMMENTS_FORM_POSTING : resolvedSubmitLabel })
80
+ ] })
81
+ ]
82
+ }
83
+ );
84
+ }
85
+
86
+ exports.CommentForm = CommentForm;