@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,143 @@
1
+ /**
2
+ * Internal query key constants for the Comments plugin.
3
+ * Shared between query-keys.ts (HTTP path) and any SSG/direct DB path
4
+ * to prevent key drift between loaders and prefetch calls.
5
+ */
6
+
7
+ export interface CommentsListDiscriminator {
8
+ resourceId: string | undefined;
9
+ resourceType: string | undefined;
10
+ parentId: string | null | undefined;
11
+ status: string | undefined;
12
+ currentUserId: string | undefined;
13
+ authorId: string | undefined;
14
+ sort: string | undefined;
15
+ limit: number;
16
+ offset: number;
17
+ }
18
+
19
+ /**
20
+ * Builds the discriminator object for the comments list query key.
21
+ */
22
+ export function commentsListDiscriminator(params?: {
23
+ resourceId?: string;
24
+ resourceType?: string;
25
+ parentId?: string | null;
26
+ status?: string;
27
+ currentUserId?: string;
28
+ authorId?: string;
29
+ sort?: string;
30
+ limit?: number;
31
+ offset?: number;
32
+ }): CommentsListDiscriminator {
33
+ return {
34
+ resourceId: params?.resourceId,
35
+ resourceType: params?.resourceType,
36
+ parentId: params?.parentId,
37
+ status: params?.status,
38
+ currentUserId: params?.currentUserId,
39
+ authorId: params?.authorId,
40
+ sort: params?.sort,
41
+ limit: params?.limit ?? 20,
42
+ offset: params?.offset ?? 0,
43
+ };
44
+ }
45
+
46
+ export interface CommentCountDiscriminator {
47
+ resourceId: string;
48
+ resourceType: string;
49
+ status: string | undefined;
50
+ }
51
+
52
+ export function commentCountDiscriminator(params: {
53
+ resourceId: string;
54
+ resourceType: string;
55
+ status?: string;
56
+ }): CommentCountDiscriminator {
57
+ return {
58
+ resourceId: params.resourceId,
59
+ resourceType: params.resourceType,
60
+ status: params.status,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Discriminator for the infinite thread query (top-level comments only).
66
+ * Intentionally excludes `offset` — pages are driven by `pageParam`, not the key.
67
+ */
68
+ export interface CommentsThreadDiscriminator {
69
+ resourceId: string | undefined;
70
+ resourceType: string | undefined;
71
+ parentId: string | null | undefined;
72
+ status: string | undefined;
73
+ currentUserId: string | undefined;
74
+ limit: number;
75
+ }
76
+
77
+ export function commentsThreadDiscriminator(params?: {
78
+ resourceId?: string;
79
+ resourceType?: string;
80
+ parentId?: string | null;
81
+ status?: string;
82
+ currentUserId?: string;
83
+ limit?: number;
84
+ }): CommentsThreadDiscriminator {
85
+ return {
86
+ resourceId: params?.resourceId,
87
+ resourceType: params?.resourceType,
88
+ parentId: params?.parentId,
89
+ status: params?.status,
90
+ currentUserId: params?.currentUserId,
91
+ limit: params?.limit ?? 20,
92
+ };
93
+ }
94
+
95
+ /** Full query key builders — use with queryClient.setQueryData() */
96
+ export const COMMENTS_QUERY_KEYS = {
97
+ /**
98
+ * Key for comments list query.
99
+ * Full key: ["comments", "list", { resourceId, resourceType, parentId, status, currentUserId, limit, offset }]
100
+ */
101
+ commentsList: (params?: {
102
+ resourceId?: string;
103
+ resourceType?: string;
104
+ parentId?: string | null;
105
+ status?: string;
106
+ currentUserId?: string;
107
+ authorId?: string;
108
+ sort?: string;
109
+ limit?: number;
110
+ offset?: number;
111
+ }) => ["comments", "list", commentsListDiscriminator(params)] as const,
112
+
113
+ /**
114
+ * Key for a single comment detail query.
115
+ * Full key: ["comments", "detail", id]
116
+ */
117
+ commentDetail: (id: string) => ["comments", "detail", id] as const,
118
+
119
+ /**
120
+ * Key for comment count query.
121
+ * Full key: ["comments", "count", { resourceId, resourceType, status }]
122
+ */
123
+ commentCount: (params: {
124
+ resourceId: string;
125
+ resourceType: string;
126
+ status?: string;
127
+ }) => ["comments", "count", commentCountDiscriminator(params)] as const,
128
+
129
+ /**
130
+ * Key for the infinite thread query (top-level comments, load-more).
131
+ * Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
132
+ * Offset is excluded — it is driven by `pageParam`, not baked into the key.
133
+ */
134
+ commentsThread: (params?: {
135
+ resourceId?: string;
136
+ resourceType?: string;
137
+ parentId?: string | null;
138
+ status?: string;
139
+ currentUserId?: string;
140
+ limit?: number;
141
+ }) =>
142
+ ["commentsThread", "list", commentsThreadDiscriminator(params)] as const,
143
+ };
@@ -0,0 +1,37 @@
1
+ import type { Comment, SerializedComment } from "../types";
2
+
3
+ /**
4
+ * Serialize a raw Comment DB record into a SerializedComment for SSG/setQueryData.
5
+ * Note: resolvedAuthorName, resolvedAvatarUrl, and isLikedByCurrentUser are not
6
+ * available from the DB record alone — use getters.ts enrichment for those.
7
+ * This serializer is for cases where you already have a SerializedComment from
8
+ * the HTTP layer and just need a type-safe round-trip.
9
+ *
10
+ * Pure function — no DB access, no hooks.
11
+ */
12
+ export function serializeComment(comment: Comment): Omit<
13
+ SerializedComment,
14
+ "resolvedAuthorName" | "resolvedAvatarUrl" | "isLikedByCurrentUser"
15
+ > & {
16
+ resolvedAuthorName: string;
17
+ resolvedAvatarUrl: null;
18
+ isLikedByCurrentUser: false;
19
+ } {
20
+ return {
21
+ id: comment.id,
22
+ resourceId: comment.resourceId,
23
+ resourceType: comment.resourceType,
24
+ parentId: comment.parentId ?? null,
25
+ authorId: comment.authorId,
26
+ resolvedAuthorName: "[deleted]",
27
+ resolvedAvatarUrl: null,
28
+ isLikedByCurrentUser: false,
29
+ body: comment.body,
30
+ status: comment.status,
31
+ likes: comment.likes,
32
+ editedAt: comment.editedAt?.toISOString() ?? null,
33
+ createdAt: comment.createdAt.toISOString(),
34
+ updatedAt: comment.updatedAt.toISOString(),
35
+ replyCount: 0,
36
+ };
37
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { MessageSquare } from "lucide-react";
4
+ import { useCommentCount } from "../hooks/use-comments";
5
+
6
+ export interface CommentCountProps {
7
+ resourceId: string;
8
+ resourceType: string;
9
+ /** Only count approved comments (default) */
10
+ status?: "pending" | "approved" | "spam";
11
+ apiBaseURL: string;
12
+ apiBasePath: string;
13
+ headers?: HeadersInit;
14
+ /** Optional className for the wrapper span */
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * Lightweight badge showing the comment count for a resource.
20
+ * Does not mount a full comment thread — suitable for post list cards.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <CommentCount
25
+ * resourceId={post.slug}
26
+ * resourceType="blog-post"
27
+ * apiBaseURL="https://example.com"
28
+ * apiBasePath="/api/data"
29
+ * />
30
+ * ```
31
+ */
32
+ export function CommentCount({
33
+ resourceId,
34
+ resourceType,
35
+ status = "approved",
36
+ apiBaseURL,
37
+ apiBasePath,
38
+ headers,
39
+ className,
40
+ }: CommentCountProps) {
41
+ const { count, isLoading } = useCommentCount(
42
+ { apiBaseURL, apiBasePath, headers },
43
+ { resourceId, resourceType, status },
44
+ );
45
+
46
+ if (isLoading) {
47
+ return (
48
+ <span
49
+ className={`inline-flex items-center gap-1 text-xs text-muted-foreground ${className ?? ""}`}
50
+ >
51
+ <MessageSquare className="h-3.5 w-3.5" />
52
+ <span className="animate-pulse">…</span>
53
+ </span>
54
+ );
55
+ }
56
+
57
+ return (
58
+ <span
59
+ className={`inline-flex items-center gap-1 text-xs text-muted-foreground ${className ?? ""}`}
60
+ data-testid="comment-count"
61
+ >
62
+ <MessageSquare className="h-3.5 w-3.5" />
63
+ {count}
64
+ </span>
65
+ );
66
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ import { useState, type ComponentType } from "react";
4
+ import { Button } from "@workspace/ui/components/button";
5
+ import { Textarea } from "@workspace/ui/components/textarea";
6
+ import {
7
+ COMMENTS_LOCALIZATION,
8
+ type CommentsLocalization,
9
+ } from "../localization";
10
+
11
+ export interface CommentFormProps {
12
+ /** Current user's ID — required to post */
13
+ authorId: string;
14
+ /** Optional parent comment ID for replies */
15
+ parentId?: string | null;
16
+ /** Initial body value (for editing) */
17
+ initialBody?: string;
18
+ /** Label for the submit button */
19
+ submitLabel?: string;
20
+ /** Called when form is submitted */
21
+ onSubmit: (body: string) => Promise<void>;
22
+ /** Called when cancel is clicked (shows Cancel button when provided) */
23
+ onCancel?: () => void;
24
+ /** Custom input component — defaults to a plain Textarea */
25
+ InputComponent?: ComponentType<{
26
+ value: string;
27
+ onChange: (value: string) => void;
28
+ disabled?: boolean;
29
+ placeholder?: string;
30
+ }>;
31
+ /** Localization strings */
32
+ localization?: Partial<CommentsLocalization>;
33
+ }
34
+
35
+ export function CommentForm({
36
+ authorId: _authorId,
37
+ initialBody = "",
38
+ submitLabel,
39
+ onSubmit,
40
+ onCancel,
41
+ InputComponent,
42
+ localization: localizationProp,
43
+ }: CommentFormProps) {
44
+ const loc = { ...COMMENTS_LOCALIZATION, ...localizationProp };
45
+ const [body, setBody] = useState(initialBody);
46
+ const [isPending, setIsPending] = useState(false);
47
+ const [error, setError] = useState<string | null>(null);
48
+
49
+ const resolvedSubmitLabel = submitLabel ?? loc.COMMENTS_FORM_POST_COMMENT;
50
+
51
+ const handleSubmit = async (e: React.FormEvent) => {
52
+ e.preventDefault();
53
+ if (!body.trim()) return;
54
+ setError(null);
55
+ setIsPending(true);
56
+ try {
57
+ await onSubmit(body.trim());
58
+ setBody("");
59
+ } catch (err) {
60
+ setError(
61
+ err instanceof Error ? err.message : loc.COMMENTS_FORM_SUBMIT_ERROR,
62
+ );
63
+ } finally {
64
+ setIsPending(false);
65
+ }
66
+ };
67
+
68
+ return (
69
+ <form
70
+ data-testid="comment-form"
71
+ onSubmit={handleSubmit}
72
+ className="flex flex-col gap-2"
73
+ >
74
+ {InputComponent ? (
75
+ <InputComponent
76
+ value={body}
77
+ onChange={setBody}
78
+ disabled={isPending}
79
+ placeholder={loc.COMMENTS_FORM_PLACEHOLDER}
80
+ />
81
+ ) : (
82
+ <Textarea
83
+ value={body}
84
+ onChange={(e) => setBody(e.target.value)}
85
+ placeholder={loc.COMMENTS_FORM_PLACEHOLDER}
86
+ disabled={isPending}
87
+ rows={3}
88
+ className="resize-none"
89
+ />
90
+ )}
91
+
92
+ {error && <p className="text-sm text-destructive">{error}</p>}
93
+
94
+ <div className="flex gap-2 justify-end">
95
+ {onCancel && (
96
+ <Button
97
+ type="button"
98
+ variant="ghost"
99
+ size="sm"
100
+ onClick={onCancel}
101
+ disabled={isPending}
102
+ >
103
+ {loc.COMMENTS_FORM_CANCEL}
104
+ </Button>
105
+ )}
106
+ <Button type="submit" size="sm" disabled={isPending || !body.trim()}>
107
+ {isPending ? loc.COMMENTS_FORM_POSTING : resolvedSubmitLabel}
108
+ </Button>
109
+ </div>
110
+ </form>
111
+ );
112
+ }