@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,367 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from "@workspace/ui/components/table";
12
+ import {
13
+ AlertDialog,
14
+ AlertDialogAction,
15
+ AlertDialogCancel,
16
+ AlertDialogContent,
17
+ AlertDialogDescription,
18
+ AlertDialogFooter,
19
+ AlertDialogHeader,
20
+ AlertDialogTitle,
21
+ } from "@workspace/ui/components/alert-dialog";
22
+ import { Button } from "@workspace/ui/components/button";
23
+ import { Badge } from "@workspace/ui/components/badge";
24
+ import {
25
+ Avatar,
26
+ AvatarFallback,
27
+ AvatarImage,
28
+ } from "@workspace/ui/components/avatar";
29
+ import { Trash2, ExternalLink, LogIn, MessageSquareOff } from "lucide-react";
30
+ import { toast } from "sonner";
31
+ import { formatDistanceToNow } from "date-fns";
32
+ import type { CommentsPluginOverrides } from "../../overrides";
33
+ import { PaginationControls } from "@workspace/ui/components/pagination-controls";
34
+ import type { SerializedComment, CommentStatus } from "../../../types";
35
+ import {
36
+ useSuspenseComments,
37
+ useDeleteComment,
38
+ } from "../../hooks/use-comments";
39
+ import {
40
+ COMMENTS_LOCALIZATION,
41
+ type CommentsLocalization,
42
+ } from "../../localization";
43
+ import { getInitials, useResolvedCurrentUserId } from "../../utils";
44
+
45
+ const PAGE_LIMIT = 20;
46
+
47
+ interface UserCommentsPageProps {
48
+ apiBaseURL: string;
49
+ apiBasePath: string;
50
+ headers?: HeadersInit;
51
+ currentUserId?: CommentsPluginOverrides["currentUserId"];
52
+ resourceLinks?: CommentsPluginOverrides["resourceLinks"];
53
+ localization?: CommentsLocalization;
54
+ }
55
+
56
+ function StatusBadge({
57
+ status,
58
+ loc,
59
+ }: {
60
+ status: CommentStatus;
61
+ loc: CommentsLocalization;
62
+ }) {
63
+ if (status === "approved") {
64
+ return (
65
+ <Badge variant="outline" className="text-green-700 border-green-300">
66
+ {loc.COMMENTS_MY_STATUS_APPROVED}
67
+ </Badge>
68
+ );
69
+ }
70
+ if (status === "pending") {
71
+ return (
72
+ <Badge variant="outline" className="text-yellow-700 border-yellow-300">
73
+ {loc.COMMENTS_MY_STATUS_PENDING}
74
+ </Badge>
75
+ );
76
+ }
77
+ return (
78
+ <Badge variant="outline" className="text-red-700 border-red-300">
79
+ {loc.COMMENTS_MY_STATUS_SPAM}
80
+ </Badge>
81
+ );
82
+ }
83
+
84
+ // ─── Main export ──────────────────────────────────────────────────────────────
85
+
86
+ export function UserCommentsPage({
87
+ apiBaseURL,
88
+ apiBasePath,
89
+ headers,
90
+ currentUserId: currentUserIdProp,
91
+ resourceLinks,
92
+ localization: localizationProp,
93
+ }: UserCommentsPageProps) {
94
+ const loc = { ...COMMENTS_LOCALIZATION, ...localizationProp };
95
+ const resolvedUserId = useResolvedCurrentUserId(currentUserIdProp);
96
+
97
+ if (!resolvedUserId) {
98
+ return (
99
+ <div
100
+ className="flex flex-col items-center justify-center gap-4 py-20 text-center"
101
+ data-testid="my-comments-login-prompt"
102
+ >
103
+ <LogIn className="h-10 w-10 text-muted-foreground" />
104
+ <p className="text-lg font-medium">{loc.COMMENTS_MY_LOGIN_TITLE}</p>
105
+ <p className="text-sm text-muted-foreground">
106
+ {loc.COMMENTS_MY_LOGIN_DESCRIPTION}
107
+ </p>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <UserCommentsList
114
+ apiBaseURL={apiBaseURL}
115
+ apiBasePath={apiBasePath}
116
+ headers={headers}
117
+ currentUserId={resolvedUserId}
118
+ resourceLinks={resourceLinks}
119
+ loc={loc}
120
+ />
121
+ );
122
+ }
123
+
124
+ // ─── List (suspense boundary is in ComposedRoute) ─────────────────────────────
125
+
126
+ function UserCommentsList({
127
+ apiBaseURL,
128
+ apiBasePath,
129
+ headers,
130
+ currentUserId,
131
+ resourceLinks,
132
+ loc,
133
+ }: {
134
+ apiBaseURL: string;
135
+ apiBasePath: string;
136
+ headers?: HeadersInit;
137
+ currentUserId: string;
138
+ resourceLinks?: CommentsPluginOverrides["resourceLinks"];
139
+ loc: CommentsLocalization;
140
+ }) {
141
+ const [page, setPage] = useState(1);
142
+ const [deleteId, setDeleteId] = useState<string | null>(null);
143
+
144
+ const config = { apiBaseURL, apiBasePath, headers };
145
+ const offset = (page - 1) * PAGE_LIMIT;
146
+
147
+ const { comments, total, refetch } = useSuspenseComments(config, {
148
+ authorId: currentUserId,
149
+ sort: "desc",
150
+ limit: PAGE_LIMIT,
151
+ offset,
152
+ });
153
+
154
+ const deleteMutation = useDeleteComment(config);
155
+
156
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_LIMIT));
157
+
158
+ const handleDelete = async () => {
159
+ if (!deleteId) return;
160
+ try {
161
+ await deleteMutation.mutateAsync(deleteId);
162
+ toast.success(loc.COMMENTS_MY_TOAST_DELETED);
163
+ refetch();
164
+ } catch {
165
+ toast.error(loc.COMMENTS_MY_TOAST_DELETE_ERROR);
166
+ } finally {
167
+ setDeleteId(null);
168
+ }
169
+ };
170
+
171
+ if (comments.length === 0 && page === 1) {
172
+ return (
173
+ <div
174
+ className="flex flex-col items-center justify-center gap-4 py-20 text-center"
175
+ data-testid="my-comments-empty"
176
+ >
177
+ <MessageSquareOff className="h-10 w-10 text-muted-foreground" />
178
+ <p className="text-lg font-medium">{loc.COMMENTS_MY_EMPTY_TITLE}</p>
179
+ <p className="text-sm text-muted-foreground">
180
+ {loc.COMMENTS_MY_EMPTY_DESCRIPTION}
181
+ </p>
182
+ </div>
183
+ );
184
+ }
185
+
186
+ return (
187
+ <div data-testid="my-comments-page" className="space-y-4">
188
+ <div>
189
+ <h1 className="text-2xl font-bold tracking-tight">
190
+ {loc.COMMENTS_MY_PAGE_TITLE}
191
+ </h1>
192
+ <p className="text-sm text-muted-foreground mt-1">
193
+ {total} {loc.COMMENTS_MY_COL_COMMENT.toLowerCase()}
194
+ {total !== 1 ? "s" : ""}
195
+ </p>
196
+ </div>
197
+
198
+ <div
199
+ className="rounded-lg border overflow-hidden"
200
+ data-testid="my-comments-list"
201
+ >
202
+ <Table>
203
+ <TableHeader>
204
+ <TableRow>
205
+ <TableHead className="w-10" />
206
+ <TableHead>{loc.COMMENTS_MY_COL_COMMENT}</TableHead>
207
+ <TableHead className="hidden sm:table-cell w-32">
208
+ {loc.COMMENTS_MY_COL_RESOURCE}
209
+ </TableHead>
210
+ <TableHead className="w-28">
211
+ {loc.COMMENTS_MY_COL_STATUS}
212
+ </TableHead>
213
+ <TableHead className="hidden md:table-cell w-36">
214
+ {loc.COMMENTS_MY_COL_DATE}
215
+ </TableHead>
216
+ <TableHead className="w-16" />
217
+ </TableRow>
218
+ </TableHeader>
219
+ <TableBody>
220
+ {comments.map((comment) => (
221
+ <CommentRow
222
+ key={comment.id}
223
+ comment={comment}
224
+ resourceLinks={resourceLinks}
225
+ loc={loc}
226
+ onDelete={() => setDeleteId(comment.id)}
227
+ isDeleting={deleteMutation.isPending && deleteId === comment.id}
228
+ />
229
+ ))}
230
+ </TableBody>
231
+ </Table>
232
+
233
+ <PaginationControls
234
+ currentPage={page}
235
+ totalPages={totalPages}
236
+ total={total}
237
+ limit={PAGE_LIMIT}
238
+ offset={offset}
239
+ onPageChange={(p) => {
240
+ setPage(p);
241
+ window.scrollTo({ top: 0, behavior: "smooth" });
242
+ }}
243
+ />
244
+ </div>
245
+
246
+ <AlertDialog
247
+ open={!!deleteId}
248
+ onOpenChange={(open) => !open && setDeleteId(null)}
249
+ >
250
+ <AlertDialogContent>
251
+ <AlertDialogHeader>
252
+ <AlertDialogTitle>{loc.COMMENTS_MY_DELETE_TITLE}</AlertDialogTitle>
253
+ <AlertDialogDescription>
254
+ {loc.COMMENTS_MY_DELETE_DESCRIPTION}
255
+ </AlertDialogDescription>
256
+ </AlertDialogHeader>
257
+ <AlertDialogFooter>
258
+ <AlertDialogCancel>
259
+ {loc.COMMENTS_MY_DELETE_CANCEL}
260
+ </AlertDialogCancel>
261
+ <AlertDialogAction
262
+ onClick={handleDelete}
263
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
264
+ >
265
+ {loc.COMMENTS_MY_DELETE_CONFIRM}
266
+ </AlertDialogAction>
267
+ </AlertDialogFooter>
268
+ </AlertDialogContent>
269
+ </AlertDialog>
270
+ </div>
271
+ );
272
+ }
273
+
274
+ // ─── Row ──────────────────────────────────────────────────────────────────────
275
+
276
+ function CommentRow({
277
+ comment,
278
+ resourceLinks,
279
+ loc,
280
+ onDelete,
281
+ isDeleting,
282
+ }: {
283
+ comment: SerializedComment;
284
+ resourceLinks?: CommentsPluginOverrides["resourceLinks"];
285
+ loc: CommentsLocalization;
286
+ onDelete: () => void;
287
+ isDeleting: boolean;
288
+ }) {
289
+ const resourceUrlBase = resourceLinks?.[comment.resourceType]?.(
290
+ comment.resourceId,
291
+ );
292
+ const resourceUrl = resourceUrlBase
293
+ ? `${resourceUrlBase}#comments`
294
+ : undefined;
295
+
296
+ return (
297
+ <TableRow data-testid="my-comment-row">
298
+ <TableCell>
299
+ <Avatar className="h-7 w-7">
300
+ {comment.resolvedAvatarUrl && (
301
+ <AvatarImage
302
+ src={comment.resolvedAvatarUrl}
303
+ alt={comment.resolvedAuthorName}
304
+ />
305
+ )}
306
+ <AvatarFallback className="text-xs">
307
+ {getInitials(comment.resolvedAuthorName)}
308
+ </AvatarFallback>
309
+ </Avatar>
310
+ </TableCell>
311
+
312
+ <TableCell className="max-w-xs">
313
+ <p className="text-sm line-clamp-2">{comment.body}</p>
314
+ {comment.parentId && (
315
+ <span className="text-xs text-muted-foreground mt-0.5 block">
316
+ {loc.COMMENTS_MY_REPLY_INDICATOR}
317
+ </span>
318
+ )}
319
+ </TableCell>
320
+
321
+ <TableCell className="hidden sm:table-cell">
322
+ <div className="flex flex-col gap-0.5">
323
+ <span className="text-xs font-medium text-muted-foreground capitalize">
324
+ {comment.resourceType.replace(/-/g, " ")}
325
+ </span>
326
+ {resourceUrl ? (
327
+ <a
328
+ href={resourceUrl}
329
+ className="text-xs text-primary hover:underline inline-flex items-center gap-1"
330
+ target="_blank"
331
+ rel="noopener noreferrer"
332
+ >
333
+ {loc.COMMENTS_MY_VIEW_LINK}
334
+ <ExternalLink className="h-3 w-3" />
335
+ </a>
336
+ ) : (
337
+ <span className="text-xs text-muted-foreground truncate max-w-[100px]">
338
+ {comment.resourceId}
339
+ </span>
340
+ )}
341
+ </div>
342
+ </TableCell>
343
+
344
+ <TableCell>
345
+ <StatusBadge status={comment.status} loc={loc} />
346
+ </TableCell>
347
+
348
+ <TableCell className="hidden md:table-cell text-xs text-muted-foreground">
349
+ {formatDistanceToNow(new Date(comment.createdAt), { addSuffix: true })}
350
+ </TableCell>
351
+
352
+ <TableCell>
353
+ <Button
354
+ variant="ghost"
355
+ size="icon"
356
+ className="h-7 w-7 text-muted-foreground hover:text-destructive"
357
+ onClick={onDelete}
358
+ disabled={isDeleting}
359
+ data-testid="my-comment-delete-button"
360
+ >
361
+ <Trash2 className="h-4 w-4" />
362
+ <span className="sr-only">{loc.COMMENTS_MY_DELETE_BUTTON_SR}</span>
363
+ </Button>
364
+ </TableCell>
365
+ </TableRow>
366
+ );
367
+ }
@@ -0,0 +1,72 @@
1
+ "use client";
2
+
3
+ import { lazy } from "react";
4
+ import { ComposedRoute } from "@btst/stack/client/components";
5
+ import { usePluginOverrides } from "@btst/stack/context";
6
+ import type { CommentsPluginOverrides } from "../../overrides";
7
+ import { COMMENTS_LOCALIZATION } from "../../localization";
8
+ import { useRouteLifecycle } from "@workspace/ui/hooks/use-route-lifecycle";
9
+ import { PageWrapper } from "../shared/page-wrapper";
10
+
11
+ const UserCommentsPageInternal = lazy(() =>
12
+ import("./my-comments-page.internal").then((m) => ({
13
+ default: m.UserCommentsPage,
14
+ })),
15
+ );
16
+
17
+ function UserCommentsPageSkeleton() {
18
+ return (
19
+ <div className="w-full max-w-3xl mx-auto space-y-4 animate-pulse">
20
+ <div className="h-8 w-48 rounded bg-muted" />
21
+ <div className="h-4 w-64 rounded bg-muted" />
22
+ <div className="rounded-lg border h-96 bg-muted" />
23
+ </div>
24
+ );
25
+ }
26
+
27
+ export function UserCommentsPageComponent() {
28
+ return (
29
+ <ComposedRoute
30
+ path="/comments"
31
+ PageComponent={UserCommentsPageWrapper}
32
+ LoadingComponent={UserCommentsPageSkeleton}
33
+ onError={(error) =>
34
+ console.error("[btst/comments] User Comments error:", error)
35
+ }
36
+ />
37
+ );
38
+ }
39
+
40
+ function UserCommentsPageWrapper() {
41
+ const overrides = usePluginOverrides<CommentsPluginOverrides>("comments");
42
+ const loc = { ...COMMENTS_LOCALIZATION, ...overrides.localization };
43
+
44
+ useRouteLifecycle({
45
+ routeName: "userComments",
46
+ context: {
47
+ path: "/comments",
48
+ isSSR: typeof window === "undefined",
49
+ },
50
+ overrides,
51
+ beforeRenderHook: (o, context) => {
52
+ if (o.onBeforeUserCommentsPageRendered) {
53
+ const result = o.onBeforeUserCommentsPageRendered(context);
54
+ return result === false ? false : true;
55
+ }
56
+ return true;
57
+ },
58
+ });
59
+
60
+ return (
61
+ <PageWrapper>
62
+ <UserCommentsPageInternal
63
+ apiBaseURL={overrides.apiBaseURL}
64
+ apiBasePath={overrides.apiBasePath}
65
+ headers={overrides.headers as HeadersInit | undefined}
66
+ currentUserId={overrides.currentUserId}
67
+ resourceLinks={overrides.resourceLinks}
68
+ localization={loc}
69
+ />
70
+ </PageWrapper>
71
+ );
72
+ }
@@ -0,0 +1,225 @@
1
+ "use client";
2
+
3
+ import type { SerializedComment } from "../../../types";
4
+ import {
5
+ useSuspenseComments,
6
+ useUpdateCommentStatus,
7
+ useDeleteComment,
8
+ } from "../../hooks/use-comments";
9
+ import { CommentThread } from "../comment-thread";
10
+ import { Button } from "@workspace/ui/components/button";
11
+ import { Badge } from "@workspace/ui/components/badge";
12
+ import {
13
+ Avatar,
14
+ AvatarFallback,
15
+ AvatarImage,
16
+ } from "@workspace/ui/components/avatar";
17
+ import { CheckCircle, ShieldOff, Trash2 } from "lucide-react";
18
+ import { formatDistanceToNow } from "date-fns";
19
+ import { toast } from "sonner";
20
+ import {
21
+ COMMENTS_LOCALIZATION,
22
+ type CommentsLocalization,
23
+ } from "../../localization";
24
+ import { getInitials } from "../../utils";
25
+
26
+ interface ResourceCommentsPageProps {
27
+ resourceId: string;
28
+ resourceType: string;
29
+ apiBaseURL: string;
30
+ apiBasePath: string;
31
+ headers?: HeadersInit;
32
+ currentUserId?: string;
33
+ loginHref?: string;
34
+ localization?: CommentsLocalization;
35
+ }
36
+
37
+ export function ResourceCommentsPage({
38
+ resourceId,
39
+ resourceType,
40
+ apiBaseURL,
41
+ apiBasePath,
42
+ headers,
43
+ currentUserId,
44
+ loginHref,
45
+ localization: localizationProp,
46
+ }: ResourceCommentsPageProps) {
47
+ const loc = { ...COMMENTS_LOCALIZATION, ...localizationProp };
48
+ const config = { apiBaseURL, apiBasePath, headers };
49
+
50
+ const {
51
+ comments: pendingComments,
52
+ total: pendingTotal,
53
+ refetch,
54
+ } = useSuspenseComments(config, {
55
+ resourceId,
56
+ resourceType,
57
+ status: "pending",
58
+ });
59
+
60
+ const updateStatus = useUpdateCommentStatus(config);
61
+ const deleteMutation = useDeleteComment(config);
62
+
63
+ const handleApprove = async (id: string) => {
64
+ try {
65
+ await updateStatus.mutateAsync({ id, status: "approved" });
66
+ toast.success(loc.COMMENTS_RESOURCE_TOAST_APPROVED);
67
+ refetch();
68
+ } catch {
69
+ toast.error(loc.COMMENTS_RESOURCE_TOAST_APPROVE_ERROR);
70
+ }
71
+ };
72
+
73
+ const handleSpam = async (id: string) => {
74
+ try {
75
+ await updateStatus.mutateAsync({ id, status: "spam" });
76
+ toast.success(loc.COMMENTS_RESOURCE_TOAST_SPAM);
77
+ refetch();
78
+ } catch {
79
+ toast.error(loc.COMMENTS_RESOURCE_TOAST_SPAM_ERROR);
80
+ }
81
+ };
82
+
83
+ const handleDelete = async (id: string) => {
84
+ if (!window.confirm(loc.COMMENTS_RESOURCE_DELETE_CONFIRM)) return;
85
+ try {
86
+ await deleteMutation.mutateAsync(id);
87
+ toast.success(loc.COMMENTS_RESOURCE_TOAST_DELETED);
88
+ refetch();
89
+ } catch {
90
+ toast.error(loc.COMMENTS_RESOURCE_TOAST_DELETE_ERROR);
91
+ }
92
+ };
93
+
94
+ return (
95
+ <div
96
+ className="w-full max-w-3xl space-y-8"
97
+ data-testid="resource-comments-page"
98
+ >
99
+ <div>
100
+ <h1 className="text-2xl font-bold">{loc.COMMENTS_RESOURCE_TITLE}</h1>
101
+ <p className="text-muted-foreground text-sm mt-1">
102
+ {resourceType}/{resourceId}
103
+ </p>
104
+ </div>
105
+
106
+ {pendingTotal > 0 && (
107
+ <div className="space-y-3">
108
+ <h2 className="text-base font-semibold flex items-center gap-2">
109
+ {loc.COMMENTS_RESOURCE_PENDING_SECTION}
110
+ <Badge variant="secondary">{pendingTotal}</Badge>
111
+ </h2>
112
+ <div className="divide-y divide-border rounded-lg border">
113
+ {pendingComments.map((comment) => (
114
+ <PendingCommentRow
115
+ key={comment.id}
116
+ comment={comment}
117
+ loc={loc}
118
+ onApprove={() => handleApprove(comment.id)}
119
+ onSpam={() => handleSpam(comment.id)}
120
+ onDelete={() => handleDelete(comment.id)}
121
+ isUpdating={updateStatus.isPending}
122
+ isDeleting={deleteMutation.isPending}
123
+ />
124
+ ))}
125
+ </div>
126
+ </div>
127
+ )}
128
+
129
+ <div>
130
+ <h2 className="text-base font-semibold mb-4">
131
+ {loc.COMMENTS_RESOURCE_THREAD_SECTION}
132
+ </h2>
133
+ <CommentThread
134
+ resourceId={resourceId}
135
+ resourceType={resourceType}
136
+ apiBaseURL={apiBaseURL}
137
+ apiBasePath={apiBasePath}
138
+ headers={headers}
139
+ currentUserId={currentUserId}
140
+ loginHref={loginHref}
141
+ localization={loc}
142
+ />
143
+ </div>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ function PendingCommentRow({
149
+ comment,
150
+ loc,
151
+ onApprove,
152
+ onSpam,
153
+ onDelete,
154
+ isUpdating,
155
+ isDeleting,
156
+ }: {
157
+ comment: SerializedComment;
158
+ loc: CommentsLocalization;
159
+ onApprove: () => void;
160
+ onSpam: () => void;
161
+ onDelete: () => void;
162
+ isUpdating: boolean;
163
+ isDeleting: boolean;
164
+ }) {
165
+ return (
166
+ <div className="flex gap-3 p-4" data-testid="pending-comment-row">
167
+ <Avatar className="h-8 w-8 shrink-0 mt-0.5">
168
+ {comment.resolvedAvatarUrl && (
169
+ <AvatarImage src={comment.resolvedAvatarUrl} />
170
+ )}
171
+ <AvatarFallback className="text-xs">
172
+ {getInitials(comment.resolvedAuthorName)}
173
+ </AvatarFallback>
174
+ </Avatar>
175
+ <div className="flex-1 min-w-0">
176
+ <div className="flex items-center gap-2 mb-1">
177
+ <span className="text-sm font-medium">
178
+ {comment.resolvedAuthorName}
179
+ </span>
180
+ <span className="text-xs text-muted-foreground">
181
+ {formatDistanceToNow(new Date(comment.createdAt), {
182
+ addSuffix: true,
183
+ })}
184
+ </span>
185
+ </div>
186
+ <p className="text-sm whitespace-pre-wrap break-words">
187
+ {comment.body}
188
+ </p>
189
+ <div className="flex gap-1 mt-2">
190
+ <Button
191
+ size="sm"
192
+ variant="outline"
193
+ className="h-7 text-xs text-green-600 border-green-200 hover:bg-green-50"
194
+ onClick={onApprove}
195
+ disabled={isUpdating}
196
+ data-testid="approve-button"
197
+ >
198
+ <CheckCircle className="h-3.5 w-3.5 mr-1" />
199
+ {loc.COMMENTS_RESOURCE_ACTION_APPROVE}
200
+ </Button>
201
+ <Button
202
+ size="sm"
203
+ variant="outline"
204
+ className="h-7 text-xs text-orange-500 border-orange-200 hover:bg-orange-50"
205
+ onClick={onSpam}
206
+ disabled={isUpdating}
207
+ >
208
+ <ShieldOff className="h-3.5 w-3.5 mr-1" />
209
+ {loc.COMMENTS_RESOURCE_ACTION_SPAM}
210
+ </Button>
211
+ <Button
212
+ size="sm"
213
+ variant="outline"
214
+ className="h-7 text-xs text-destructive border-destructive/30 hover:bg-destructive/5"
215
+ onClick={onDelete}
216
+ disabled={isDeleting}
217
+ >
218
+ <Trash2 className="h-3.5 w-3.5 mr-1" />
219
+ {loc.COMMENTS_RESOURCE_ACTION_DELETE}
220
+ </Button>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ );
225
+ }