doclific 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/.gitattributes +2 -0
  2. package/.prettierignore +5 -0
  3. package/.prettierrc +9 -0
  4. package/.vscode/settings.json +13 -0
  5. package/dist/bin/doclific.d.ts +3 -0
  6. package/dist/bin/doclific.d.ts.map +1 -0
  7. package/dist/bin/doclific.js +11 -0
  8. package/dist/core/codebase.js +31 -0
  9. package/dist/core/docs.js +75 -0
  10. package/dist/core/git.js +47 -0
  11. package/dist/server/index.d.ts +2 -0
  12. package/dist/server/index.d.ts.map +1 -0
  13. package/dist/server/index.js +46 -0
  14. package/dist/server/router.d.ts +9 -0
  15. package/dist/server/router.d.ts.map +1 -0
  16. package/dist/server/router.js +55 -0
  17. package/frontend/README.md +73 -0
  18. package/frontend/components.json +24 -0
  19. package/frontend/eslint.config.js +23 -0
  20. package/frontend/index.html +25 -0
  21. package/frontend/package-lock.json +15754 -0
  22. package/frontend/package.json +122 -0
  23. package/frontend/public/logo.svg +1 -0
  24. package/frontend/src/App.tsx +21 -0
  25. package/frontend/src/components/app-sidebar.tsx +393 -0
  26. package/frontend/src/components/editor/editor-base-kit.tsx +43 -0
  27. package/frontend/src/components/editor/editor-kit.tsx +93 -0
  28. package/frontend/src/components/editor/plugins/align-base-kit.tsx +16 -0
  29. package/frontend/src/components/editor/plugins/align-kit.tsx +18 -0
  30. package/frontend/src/components/editor/plugins/autoformat-kit.tsx +236 -0
  31. package/frontend/src/components/editor/plugins/basic-blocks-base-kit.tsx +35 -0
  32. package/frontend/src/components/editor/plugins/basic-blocks-kit.tsx +88 -0
  33. package/frontend/src/components/editor/plugins/basic-marks-base-kit.tsx +27 -0
  34. package/frontend/src/components/editor/plugins/basic-marks-kit.tsx +41 -0
  35. package/frontend/src/components/editor/plugins/basic-nodes-kit.tsx +6 -0
  36. package/frontend/src/components/editor/plugins/block-menu-kit.tsx +14 -0
  37. package/frontend/src/components/editor/plugins/block-placeholder-kit.tsx +17 -0
  38. package/frontend/src/components/editor/plugins/block-selection-kit.tsx +32 -0
  39. package/frontend/src/components/editor/plugins/callout-base-kit.tsx +7 -0
  40. package/frontend/src/components/editor/plugins/callout-kit.tsx +7 -0
  41. package/frontend/src/components/editor/plugins/code-block-base-kit.tsx +23 -0
  42. package/frontend/src/components/editor/plugins/code-block-kit.tsx +26 -0
  43. package/frontend/src/components/editor/plugins/codebase-kit.tsx +23 -0
  44. package/frontend/src/components/editor/plugins/column-base-kit.tsx +11 -0
  45. package/frontend/src/components/editor/plugins/column-kit.tsx +10 -0
  46. package/frontend/src/components/editor/plugins/comment-base-kit.tsx +7 -0
  47. package/frontend/src/components/editor/plugins/comment-kit.tsx +97 -0
  48. package/frontend/src/components/editor/plugins/cursor-overlay-kit.tsx +13 -0
  49. package/frontend/src/components/editor/plugins/date-base-kit.tsx +5 -0
  50. package/frontend/src/components/editor/plugins/date-kit.tsx +7 -0
  51. package/frontend/src/components/editor/plugins/discussion-kit.tsx +148 -0
  52. package/frontend/src/components/editor/plugins/dnd-kit.tsx +28 -0
  53. package/frontend/src/components/editor/plugins/docx-kit.tsx +6 -0
  54. package/frontend/src/components/editor/plugins/emoji-kit.tsx +13 -0
  55. package/frontend/src/components/editor/plugins/excalidraw-kit.tsx +9 -0
  56. package/frontend/src/components/editor/plugins/exit-break-kit.tsx +12 -0
  57. package/frontend/src/components/editor/plugins/floating-toolbar-kit.tsx +19 -0
  58. package/frontend/src/components/editor/plugins/font-base-kit.tsx +20 -0
  59. package/frontend/src/components/editor/plugins/font-kit.tsx +29 -0
  60. package/frontend/src/components/editor/plugins/indent-base-kit.tsx +19 -0
  61. package/frontend/src/components/editor/plugins/indent-kit.tsx +22 -0
  62. package/frontend/src/components/editor/plugins/line-height-base-kit.tsx +14 -0
  63. package/frontend/src/components/editor/plugins/line-height-kit.tsx +16 -0
  64. package/frontend/src/components/editor/plugins/link-base-kit.tsx +5 -0
  65. package/frontend/src/components/editor/plugins/link-kit.tsx +15 -0
  66. package/frontend/src/components/editor/plugins/list-base-kit.tsx +23 -0
  67. package/frontend/src/components/editor/plugins/list-kit.tsx +26 -0
  68. package/frontend/src/components/editor/plugins/markdown-kit.tsx +46 -0
  69. package/frontend/src/components/editor/plugins/math-base-kit.tsx +11 -0
  70. package/frontend/src/components/editor/plugins/math-kit.tsx +13 -0
  71. package/frontend/src/components/editor/plugins/media-base-kit.tsx +31 -0
  72. package/frontend/src/components/editor/plugins/media-kit.tsx +43 -0
  73. package/frontend/src/components/editor/plugins/mention-base-kit.tsx +7 -0
  74. package/frontend/src/components/editor/plugins/mention-kit.tsx +15 -0
  75. package/frontend/src/components/editor/plugins/slash-kit.tsx +18 -0
  76. package/frontend/src/components/editor/plugins/suggestion-base-kit.tsx +7 -0
  77. package/frontend/src/components/editor/plugins/suggestion-kit.tsx +90 -0
  78. package/frontend/src/components/editor/plugins/table-base-kit.tsx +20 -0
  79. package/frontend/src/components/editor/plugins/table-kit.tsx +22 -0
  80. package/frontend/src/components/editor/plugins/toc-base-kit.tsx +5 -0
  81. package/frontend/src/components/editor/plugins/toc-kit.tsx +14 -0
  82. package/frontend/src/components/editor/plugins/toggle-base-kit.tsx +7 -0
  83. package/frontend/src/components/editor/plugins/toggle-kit.tsx +11 -0
  84. package/frontend/src/components/editor/transforms.ts +194 -0
  85. package/frontend/src/components/markdown-to-slate-demo.tsx +50 -0
  86. package/frontend/src/components/mode-toggle.tsx +15 -0
  87. package/frontend/src/components/theme-provider.tsx +73 -0
  88. package/frontend/src/components/ui/alert-dialog.tsx +155 -0
  89. package/frontend/src/components/ui/align-toolbar-button.tsx +84 -0
  90. package/frontend/src/components/ui/avatar.tsx +51 -0
  91. package/frontend/src/components/ui/block-context-menu.tsx +199 -0
  92. package/frontend/src/components/ui/block-discussion.tsx +365 -0
  93. package/frontend/src/components/ui/block-draggable.tsx +512 -0
  94. package/frontend/src/components/ui/block-list-static.tsx +80 -0
  95. package/frontend/src/components/ui/block-list.tsx +87 -0
  96. package/frontend/src/components/ui/block-selection.tsx +42 -0
  97. package/frontend/src/components/ui/block-suggestion.tsx +473 -0
  98. package/frontend/src/components/ui/blockquote-node-static.tsx +11 -0
  99. package/frontend/src/components/ui/blockquote-node.tsx +13 -0
  100. package/frontend/src/components/ui/button.tsx +62 -0
  101. package/frontend/src/components/ui/calendar.tsx +218 -0
  102. package/frontend/src/components/ui/callout-node-static.tsx +36 -0
  103. package/frontend/src/components/ui/callout-node.tsx +63 -0
  104. package/frontend/src/components/ui/caption.tsx +63 -0
  105. package/frontend/src/components/ui/checkbox.tsx +30 -0
  106. package/frontend/src/components/ui/code-block-node-static.tsx +35 -0
  107. package/frontend/src/components/ui/code-block-node.tsx +287 -0
  108. package/frontend/src/components/ui/code-node-static.tsx +15 -0
  109. package/frontend/src/components/ui/code-node.tsx +17 -0
  110. package/frontend/src/components/ui/codebase-snippet-node.tsx +237 -0
  111. package/frontend/src/components/ui/column-node-static.tsx +29 -0
  112. package/frontend/src/components/ui/column-node.tsx +317 -0
  113. package/frontend/src/components/ui/command.tsx +182 -0
  114. package/frontend/src/components/ui/comment-node-static.tsx +15 -0
  115. package/frontend/src/components/ui/comment-node.tsx +45 -0
  116. package/frontend/src/components/ui/comment-toolbar-button.tsx +24 -0
  117. package/frontend/src/components/ui/comment.tsx +618 -0
  118. package/frontend/src/components/ui/context-menu.tsx +250 -0
  119. package/frontend/src/components/ui/cursor-overlay.tsx +66 -0
  120. package/frontend/src/components/ui/date-node-static.tsx +45 -0
  121. package/frontend/src/components/ui/date-node.tsx +93 -0
  122. package/frontend/src/components/ui/dialog.tsx +143 -0
  123. package/frontend/src/components/ui/dropdown-menu.tsx +255 -0
  124. package/frontend/src/components/ui/dynamic-icon.tsx +12 -0
  125. package/frontend/src/components/ui/editor-static.tsx +53 -0
  126. package/frontend/src/components/ui/editor.tsx +130 -0
  127. package/frontend/src/components/ui/emoji-node.tsx +69 -0
  128. package/frontend/src/components/ui/emoji-toolbar-button.tsx +628 -0
  129. package/frontend/src/components/ui/equation-node-static.tsx +98 -0
  130. package/frontend/src/components/ui/equation-node.tsx +235 -0
  131. package/frontend/src/components/ui/equation-toolbar-button.tsx +25 -0
  132. package/frontend/src/components/ui/excalidraw-node.tsx +36 -0
  133. package/frontend/src/components/ui/export-toolbar-button.tsx +174 -0
  134. package/frontend/src/components/ui/file-selector.tsx +339 -0
  135. package/frontend/src/components/ui/floating-toolbar-buttons.tsx +73 -0
  136. package/frontend/src/components/ui/floating-toolbar.tsx +85 -0
  137. package/frontend/src/components/ui/font-color-toolbar-button.tsx +831 -0
  138. package/frontend/src/components/ui/font-size-toolbar-button.tsx +152 -0
  139. package/frontend/src/components/ui/heading-node-static.tsx +68 -0
  140. package/frontend/src/components/ui/heading-node.tsx +58 -0
  141. package/frontend/src/components/ui/highlight-node-static.tsx +11 -0
  142. package/frontend/src/components/ui/highlight-node.tsx +13 -0
  143. package/frontend/src/components/ui/history-toolbar-button.tsx +50 -0
  144. package/frontend/src/components/ui/hr-node-static.tsx +20 -0
  145. package/frontend/src/components/ui/hr-node.tsx +33 -0
  146. package/frontend/src/components/ui/import-toolbar-button.tsx +97 -0
  147. package/frontend/src/components/ui/indent-toolbar-button.tsx +30 -0
  148. package/frontend/src/components/ui/inline-combobox.tsx +414 -0
  149. package/frontend/src/components/ui/input.tsx +21 -0
  150. package/frontend/src/components/ui/insert-toolbar-button.tsx +254 -0
  151. package/frontend/src/components/ui/kbd-node-static.tsx +15 -0
  152. package/frontend/src/components/ui/kbd-node.tsx +17 -0
  153. package/frontend/src/components/ui/layout-header.tsx +35 -0
  154. package/frontend/src/components/ui/line-height-toolbar-button.tsx +68 -0
  155. package/frontend/src/components/ui/link-node-static.tsx +21 -0
  156. package/frontend/src/components/ui/link-node.tsx +39 -0
  157. package/frontend/src/components/ui/link-toolbar-button.tsx +22 -0
  158. package/frontend/src/components/ui/link-toolbar.tsx +206 -0
  159. package/frontend/src/components/ui/list-toolbar-button.tsx +204 -0
  160. package/frontend/src/components/ui/mark-toolbar-button.tsx +19 -0
  161. package/frontend/src/components/ui/media-audio-node-static.tsx +17 -0
  162. package/frontend/src/components/ui/media-audio-node.tsx +39 -0
  163. package/frontend/src/components/ui/media-embed-node.tsx +136 -0
  164. package/frontend/src/components/ui/media-file-node-static.tsx +29 -0
  165. package/frontend/src/components/ui/media-file-node.tsx +47 -0
  166. package/frontend/src/components/ui/media-image-node-static.tsx +39 -0
  167. package/frontend/src/components/ui/media-image-node.tsx +80 -0
  168. package/frontend/src/components/ui/media-placeholder-node.tsx +249 -0
  169. package/frontend/src/components/ui/media-preview-dialog.tsx +152 -0
  170. package/frontend/src/components/ui/media-toolbar-button.tsx +225 -0
  171. package/frontend/src/components/ui/media-toolbar.tsx +115 -0
  172. package/frontend/src/components/ui/media-upload-toast.tsx +66 -0
  173. package/frontend/src/components/ui/media-video-node-static.tsx +30 -0
  174. package/frontend/src/components/ui/media-video-node.tsx +121 -0
  175. package/frontend/src/components/ui/mention-node-static.tsx +36 -0
  176. package/frontend/src/components/ui/mention-node.tsx +194 -0
  177. package/frontend/src/components/ui/mode-toolbar-button.tsx +123 -0
  178. package/frontend/src/components/ui/more-toolbar-button.tsx +80 -0
  179. package/frontend/src/components/ui/paragraph-node-static.tsx +13 -0
  180. package/frontend/src/components/ui/paragraph-node.tsx +15 -0
  181. package/frontend/src/components/ui/popover.tsx +46 -0
  182. package/frontend/src/components/ui/resize-handle.tsx +87 -0
  183. package/frontend/src/components/ui/separator.tsx +28 -0
  184. package/frontend/src/components/ui/sheet.tsx +139 -0
  185. package/frontend/src/components/ui/sidebar.tsx +726 -0
  186. package/frontend/src/components/ui/skeleton.tsx +13 -0
  187. package/frontend/src/components/ui/slash-node.tsx +233 -0
  188. package/frontend/src/components/ui/sonner.tsx +38 -0
  189. package/frontend/src/components/ui/suggestion-node-static.tsx +35 -0
  190. package/frontend/src/components/ui/suggestion-node.tsx +162 -0
  191. package/frontend/src/components/ui/suggestion-toolbar-button.tsx +25 -0
  192. package/frontend/src/components/ui/table-icons.tsx +862 -0
  193. package/frontend/src/components/ui/table-node-static.tsx +98 -0
  194. package/frontend/src/components/ui/table-node.tsx +656 -0
  195. package/frontend/src/components/ui/table-toolbar-button.tsx +264 -0
  196. package/frontend/src/components/ui/toc-node-static.tsx +92 -0
  197. package/frontend/src/components/ui/toc-node.tsx +55 -0
  198. package/frontend/src/components/ui/toggle-node-static.tsx +18 -0
  199. package/frontend/src/components/ui/toggle-node.tsx +36 -0
  200. package/frontend/src/components/ui/toggle-toolbar-button.tsx +22 -0
  201. package/frontend/src/components/ui/toolbar.tsx +387 -0
  202. package/frontend/src/components/ui/tooltip.tsx +59 -0
  203. package/frontend/src/components/ui/turn-into-toolbar-button.tsx +188 -0
  204. package/frontend/src/hooks/use-debounce.ts +18 -0
  205. package/frontend/src/hooks/use-is-touch-device.ts +24 -0
  206. package/frontend/src/hooks/use-mobile.ts +19 -0
  207. package/frontend/src/hooks/use-mounted.ts +11 -0
  208. package/frontend/src/hooks/use-upload-file.ts +128 -0
  209. package/frontend/src/index.css +128 -0
  210. package/frontend/src/layout.tsx +42 -0
  211. package/frontend/src/lib/markdown-joiner-transform.ts +239 -0
  212. package/frontend/src/lib/orpc.ts +13 -0
  213. package/frontend/src/lib/uploadthing.ts +19 -0
  214. package/frontend/src/lib/utils.ts +6 -0
  215. package/frontend/src/main.tsx +13 -0
  216. package/frontend/src/pages/editor.tsx +44 -0
  217. package/frontend/src/types/docs.d.ts +6 -0
  218. package/frontend/src/types/global.d.ts +9 -0
  219. package/frontend/src/types/router.d.ts +4 -0
  220. package/frontend/tsconfig.app.json +33 -0
  221. package/frontend/tsconfig.json +10 -0
  222. package/frontend/tsconfig.node.json +26 -0
  223. package/frontend/vite.config.ts +14 -0
  224. package/package.json +30 -0
  225. package/src/bin/doclific.ts +17 -0
  226. package/src/core/codebase.ts +39 -0
  227. package/src/core/docs.ts +90 -0
  228. package/src/core/git.ts +48 -0
  229. package/src/server/index.ts +55 -0
  230. package/src/server/router.ts +65 -0
  231. package/tsconfig.json +15 -0
@@ -0,0 +1,618 @@
1
+ import React from 'react';
2
+
3
+ import type { CreatePlateEditorOptions } from 'platejs/react';
4
+
5
+ import { getCommentKey, getDraftCommentKey } from '@platejs/comment';
6
+ import { CommentPlugin, useCommentId } from '@platejs/comment/react';
7
+ import {
8
+ differenceInDays,
9
+ differenceInHours,
10
+ differenceInMinutes,
11
+ format,
12
+ } from 'date-fns';
13
+ import {
14
+ ArrowUpIcon,
15
+ CheckIcon,
16
+ MoreHorizontalIcon,
17
+ PencilIcon,
18
+ TrashIcon,
19
+ XIcon,
20
+ } from 'lucide-react';
21
+ import { type Value, KEYS, nanoid, NodeApi } from 'platejs';
22
+ import {
23
+ Plate,
24
+ useEditorPlugin,
25
+ useEditorRef,
26
+ usePlateEditor,
27
+ usePluginOption,
28
+ } from 'platejs/react';
29
+
30
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
31
+ import { Button } from '@/components/ui/button';
32
+ import {
33
+ DropdownMenu,
34
+ DropdownMenuContent,
35
+ DropdownMenuGroup,
36
+ DropdownMenuItem,
37
+ DropdownMenuTrigger,
38
+ } from '@/components/ui/dropdown-menu';
39
+ import { cn } from '@/lib/utils';
40
+ import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit';
41
+ import {
42
+ type TDiscussion,
43
+ discussionPlugin,
44
+ } from '@/components/editor/plugins/discussion-kit';
45
+
46
+ import { Editor, EditorContainer } from './editor';
47
+
48
+ export type TComment = {
49
+ id: string;
50
+ contentRich: Value;
51
+ createdAt: Date;
52
+ discussionId: string;
53
+ isEdited: boolean;
54
+ userId: string;
55
+ };
56
+
57
+ export function Comment(props: {
58
+ comment: TComment;
59
+ discussionLength: number;
60
+ editingId: string | null;
61
+ index: number;
62
+ setEditingId: React.Dispatch<React.SetStateAction<string | null>>;
63
+ documentContent?: string;
64
+ showDocumentContent?: boolean;
65
+ onEditorClick?: () => void;
66
+ }) {
67
+ const {
68
+ comment,
69
+ discussionLength,
70
+ documentContent,
71
+ editingId,
72
+ index,
73
+ setEditingId,
74
+ showDocumentContent = false,
75
+ onEditorClick,
76
+ } = props;
77
+
78
+ const editor = useEditorRef();
79
+ const userInfo = usePluginOption(discussionPlugin, 'user', comment.userId);
80
+ const currentUserId = usePluginOption(discussionPlugin, 'currentUserId');
81
+
82
+ const resolveDiscussion = async (id: string) => {
83
+ const updatedDiscussions = editor
84
+ .getOption(discussionPlugin, 'discussions')
85
+ .map((discussion) => {
86
+ if (discussion.id === id) {
87
+ return { ...discussion, isResolved: true };
88
+ }
89
+ return discussion;
90
+ });
91
+ editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);
92
+ };
93
+
94
+ const removeDiscussion = async (id: string) => {
95
+ const updatedDiscussions = editor
96
+ .getOption(discussionPlugin, 'discussions')
97
+ .filter((discussion) => discussion.id !== id);
98
+ editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);
99
+ };
100
+
101
+ const updateComment = async (input: {
102
+ id: string;
103
+ contentRich: Value;
104
+ discussionId: string;
105
+ isEdited: boolean;
106
+ }) => {
107
+ const updatedDiscussions = editor
108
+ .getOption(discussionPlugin, 'discussions')
109
+ .map((discussion) => {
110
+ if (discussion.id === input.discussionId) {
111
+ const updatedComments = discussion.comments.map((comment) => {
112
+ if (comment.id === input.id) {
113
+ return {
114
+ ...comment,
115
+ contentRich: input.contentRich,
116
+ isEdited: true,
117
+ updatedAt: new Date(),
118
+ };
119
+ }
120
+ return comment;
121
+ });
122
+ return { ...discussion, comments: updatedComments };
123
+ }
124
+ return discussion;
125
+ });
126
+ editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);
127
+ };
128
+
129
+ const { tf } = useEditorPlugin(CommentPlugin);
130
+
131
+ // Replace to your own backend or refer to potion
132
+ const isMyComment = currentUserId === comment.userId;
133
+
134
+ const initialValue = comment.contentRich;
135
+
136
+ const commentEditor = useCommentEditor(
137
+ {
138
+ id: comment.id,
139
+ value: initialValue,
140
+ },
141
+ [initialValue]
142
+ );
143
+
144
+ const onCancel = () => {
145
+ setEditingId(null);
146
+ commentEditor.tf.replaceNodes(initialValue, {
147
+ at: [],
148
+ children: true,
149
+ });
150
+ };
151
+
152
+ const onSave = () => {
153
+ void updateComment({
154
+ id: comment.id,
155
+ contentRich: commentEditor.children,
156
+ discussionId: comment.discussionId,
157
+ isEdited: true,
158
+ });
159
+ setEditingId(null);
160
+ };
161
+
162
+ const onResolveComment = () => {
163
+ void resolveDiscussion(comment.discussionId);
164
+ tf.comment.unsetMark({ id: comment.discussionId });
165
+ };
166
+
167
+ const isFirst = index === 0;
168
+ const isLast = index === discussionLength - 1;
169
+ const isEditing = editingId && editingId === comment.id;
170
+
171
+ const [hovering, setHovering] = React.useState(false);
172
+ const [dropdownOpen, setDropdownOpen] = React.useState(false);
173
+
174
+ return (
175
+ <div
176
+ onMouseEnter={() => setHovering(true)}
177
+ onMouseLeave={() => setHovering(false)}
178
+ >
179
+ <div className="relative flex items-center">
180
+ <Avatar className="size-5">
181
+ <AvatarImage alt={userInfo?.name} src={userInfo?.avatarUrl} />
182
+ <AvatarFallback>{userInfo?.name?.[0]}</AvatarFallback>
183
+ </Avatar>
184
+ <h4 className="mx-2 font-semibold text-sm leading-none">
185
+ {/* Replace to your own backend or refer to potion */}
186
+ {userInfo?.name}
187
+ </h4>
188
+
189
+ <div className="text-muted-foreground/80 text-xs leading-none">
190
+ <span className="mr-1">
191
+ {formatCommentDate(new Date(comment.createdAt))}
192
+ </span>
193
+ {comment.isEdited && <span>(edited)</span>}
194
+ </div>
195
+
196
+ {isMyComment && (hovering || dropdownOpen) && (
197
+ <div className="absolute top-0 right-0 flex space-x-1">
198
+ {index === 0 && (
199
+ <Button
200
+ variant="ghost"
201
+ className="h-6 p-1 text-muted-foreground"
202
+ onClick={onResolveComment}
203
+ type="button"
204
+ >
205
+ <CheckIcon className="size-4" />
206
+ </Button>
207
+ )}
208
+
209
+ <CommentMoreDropdown
210
+ onCloseAutoFocus={() => {
211
+ setTimeout(() => {
212
+ commentEditor.tf.focus({ edge: 'endEditor' });
213
+ }, 0);
214
+ }}
215
+ onRemoveComment={() => {
216
+ if (discussionLength === 1) {
217
+ tf.comment.unsetMark({ id: comment.discussionId });
218
+ void removeDiscussion(comment.discussionId);
219
+ }
220
+ }}
221
+ comment={comment}
222
+ dropdownOpen={dropdownOpen}
223
+ setDropdownOpen={setDropdownOpen}
224
+ setEditingId={setEditingId}
225
+ />
226
+ </div>
227
+ )}
228
+ </div>
229
+
230
+ {isFirst && showDocumentContent && (
231
+ <div className="relative mt-1 flex pl-[32px] text-sm text-subtle-foreground">
232
+ {discussionLength > 1 && (
233
+ <div className="absolute top-[5px] left-3 h-full w-0.5 shrink-0 bg-muted" />
234
+ )}
235
+ <div className="my-px w-0.5 shrink-0 bg-highlight" />
236
+ {documentContent && <div className="ml-2">{documentContent}</div>}
237
+ </div>
238
+ )}
239
+
240
+ <div className="relative my-1 pl-[26px]">
241
+ {!isLast && (
242
+ <div className="absolute top-0 left-3 h-full w-0.5 shrink-0 bg-muted" />
243
+ )}
244
+ <Plate readOnly={!isEditing} editor={commentEditor}>
245
+ <EditorContainer variant="comment">
246
+ <Editor
247
+ variant="comment"
248
+ className="w-auto grow"
249
+ onClick={() => onEditorClick?.()}
250
+ />
251
+
252
+ {isEditing && (
253
+ <div className="ml-auto flex shrink-0 gap-1">
254
+ <Button
255
+ size="icon"
256
+ variant="ghost"
257
+ className="size-[28px]"
258
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
259
+ e.stopPropagation();
260
+ void onCancel();
261
+ }}
262
+ >
263
+ <div className="flex size-5 shrink-0 items-center justify-center rounded-[50%] bg-primary/40">
264
+ <XIcon className="size-3 stroke-[3px] text-background" />
265
+ </div>
266
+ </Button>
267
+
268
+ <Button
269
+ size="icon"
270
+ variant="ghost"
271
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
272
+ e.stopPropagation();
273
+ void onSave();
274
+ }}
275
+ >
276
+ <div className="flex size-5 shrink-0 items-center justify-center rounded-[50%] bg-brand">
277
+ <CheckIcon className="size-3 stroke-[3px] text-background" />
278
+ </div>
279
+ </Button>
280
+ </div>
281
+ )}
282
+ </EditorContainer>
283
+ </Plate>
284
+ </div>
285
+ </div>
286
+ );
287
+ }
288
+
289
+ function CommentMoreDropdown(props: {
290
+ comment: TComment;
291
+ dropdownOpen: boolean;
292
+ setDropdownOpen: React.Dispatch<React.SetStateAction<boolean>>;
293
+ setEditingId: React.Dispatch<React.SetStateAction<string | null>>;
294
+ onCloseAutoFocus?: () => void;
295
+ onRemoveComment?: () => void;
296
+ }) {
297
+ const {
298
+ comment,
299
+ dropdownOpen,
300
+ setDropdownOpen,
301
+ setEditingId,
302
+ onCloseAutoFocus,
303
+ onRemoveComment,
304
+ } = props;
305
+
306
+ const editor = useEditorRef();
307
+
308
+ const selectedEditCommentRef = React.useRef<boolean>(false);
309
+
310
+ const onDeleteComment = React.useCallback(() => {
311
+ if (!comment.id)
312
+ return alert('You are operating too quickly, please try again later.');
313
+
314
+ // Find and update the discussion
315
+ const updatedDiscussions = editor
316
+ .getOption(discussionPlugin, 'discussions')
317
+ .map((discussion) => {
318
+ if (discussion.id !== comment.discussionId) {
319
+ return discussion;
320
+ }
321
+
322
+ const commentIndex = discussion.comments.findIndex(
323
+ (c) => c.id === comment.id
324
+ );
325
+ if (commentIndex === -1) {
326
+ return discussion;
327
+ }
328
+
329
+ return {
330
+ ...discussion,
331
+ comments: [
332
+ ...discussion.comments.slice(0, commentIndex),
333
+ ...discussion.comments.slice(commentIndex + 1),
334
+ ],
335
+ };
336
+ });
337
+
338
+ // Save back to session storage
339
+ editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);
340
+ onRemoveComment?.();
341
+ }, [comment.discussionId, comment.id, editor, onRemoveComment]);
342
+
343
+ const onEditComment = React.useCallback(() => {
344
+ selectedEditCommentRef.current = true;
345
+
346
+ if (!comment.id)
347
+ return alert('You are operating too quickly, please try again later.');
348
+
349
+ setEditingId(comment.id);
350
+ }, [comment.id, setEditingId]);
351
+
352
+ return (
353
+ <DropdownMenu
354
+ open={dropdownOpen}
355
+ onOpenChange={setDropdownOpen}
356
+ modal={false}
357
+ >
358
+ <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
359
+ <Button variant="ghost" className={cn('h-6 p-1 text-muted-foreground')}>
360
+ <MoreHorizontalIcon className="size-4" />
361
+ </Button>
362
+ </DropdownMenuTrigger>
363
+ <DropdownMenuContent
364
+ className="w-48"
365
+ onCloseAutoFocus={(e) => {
366
+ if (selectedEditCommentRef.current) {
367
+ onCloseAutoFocus?.();
368
+ selectedEditCommentRef.current = false;
369
+ }
370
+
371
+ return e.preventDefault();
372
+ }}
373
+ >
374
+ <DropdownMenuGroup>
375
+ <DropdownMenuItem onClick={onEditComment}>
376
+ <PencilIcon className="size-4" />
377
+ Edit comment
378
+ </DropdownMenuItem>
379
+ <DropdownMenuItem onClick={onDeleteComment}>
380
+ <TrashIcon className="size-4" />
381
+ Delete comment
382
+ </DropdownMenuItem>
383
+ </DropdownMenuGroup>
384
+ </DropdownMenuContent>
385
+ </DropdownMenu>
386
+ );
387
+ }
388
+
389
+ const useCommentEditor = (
390
+ options: Omit<CreatePlateEditorOptions, 'plugins'> = {},
391
+ deps: any[] = []
392
+ ) => {
393
+ const commentEditor = usePlateEditor(
394
+ {
395
+ id: 'comment',
396
+ plugins: BasicMarksKit,
397
+ value: [],
398
+ ...options,
399
+ },
400
+ deps
401
+ );
402
+
403
+ return commentEditor;
404
+ };
405
+
406
+ export function CommentCreateForm({
407
+ autoFocus = false,
408
+ className,
409
+ discussionId: discussionIdProp,
410
+ focusOnMount = false,
411
+ }: {
412
+ autoFocus?: boolean;
413
+ className?: string;
414
+ discussionId?: string;
415
+ focusOnMount?: boolean;
416
+ }) {
417
+ const discussions = usePluginOption(discussionPlugin, 'discussions');
418
+
419
+ const editor = useEditorRef();
420
+ const commentId = useCommentId();
421
+ const discussionId = discussionIdProp ?? commentId;
422
+
423
+ const userInfo = usePluginOption(discussionPlugin, 'currentUser');
424
+ const [commentValue, setCommentValue] = React.useState<Value | undefined>();
425
+ const commentContent = React.useMemo(
426
+ () =>
427
+ commentValue
428
+ ? NodeApi.string({ children: commentValue, type: KEYS.p })
429
+ : '',
430
+ [commentValue]
431
+ );
432
+ const commentEditor = useCommentEditor();
433
+
434
+ React.useEffect(() => {
435
+ if (commentEditor && focusOnMount) {
436
+ commentEditor.tf.focus();
437
+ }
438
+ }, [commentEditor, focusOnMount]);
439
+
440
+ const onAddComment = React.useCallback(async () => {
441
+ if (!commentValue) return;
442
+
443
+ commentEditor.tf.reset();
444
+
445
+ if (discussionId) {
446
+ // Get existing discussion
447
+ const discussion = discussions.find((d) => d.id === discussionId);
448
+ if (!discussion) {
449
+ // Mock creating suggestion
450
+ const newDiscussion: TDiscussion = {
451
+ id: discussionId,
452
+ comments: [
453
+ {
454
+ id: nanoid(),
455
+ contentRich: commentValue,
456
+ createdAt: new Date(),
457
+ discussionId,
458
+ isEdited: false,
459
+ userId: editor.getOption(discussionPlugin, 'currentUserId'),
460
+ },
461
+ ],
462
+ createdAt: new Date(),
463
+ isResolved: false,
464
+ userId: editor.getOption(discussionPlugin, 'currentUserId'),
465
+ };
466
+
467
+ editor.setOption(discussionPlugin, 'discussions', [
468
+ ...discussions,
469
+ newDiscussion,
470
+ ]);
471
+ return;
472
+ }
473
+
474
+ // Create reply comment
475
+ const comment: TComment = {
476
+ id: nanoid(),
477
+ contentRich: commentValue,
478
+ createdAt: new Date(),
479
+ discussionId,
480
+ isEdited: false,
481
+ userId: editor.getOption(discussionPlugin, 'currentUserId'),
482
+ };
483
+
484
+ // Add reply to discussion comments
485
+ const updatedDiscussion = {
486
+ ...discussion,
487
+ comments: [...discussion.comments, comment],
488
+ };
489
+
490
+ // Filter out old discussion and add updated one
491
+ const updatedDiscussions = discussions
492
+ .filter((d) => d.id !== discussionId)
493
+ .concat(updatedDiscussion);
494
+
495
+ editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);
496
+
497
+ return;
498
+ }
499
+
500
+ const commentsNodeEntry = editor
501
+ .getApi(CommentPlugin)
502
+ .comment.nodes({ at: [], isDraft: true });
503
+
504
+ if (commentsNodeEntry.length === 0) return;
505
+
506
+ const documentContent = commentsNodeEntry
507
+ .map(([node]) => node.text)
508
+ .join('');
509
+
510
+ const _discussionId = nanoid();
511
+ // Mock creating new discussion
512
+ const newDiscussion: TDiscussion = {
513
+ id: _discussionId,
514
+ comments: [
515
+ {
516
+ id: nanoid(),
517
+ contentRich: commentValue,
518
+ createdAt: new Date(),
519
+ discussionId: _discussionId,
520
+ isEdited: false,
521
+ userId: editor.getOption(discussionPlugin, 'currentUserId'),
522
+ },
523
+ ],
524
+ createdAt: new Date(),
525
+ documentContent,
526
+ isResolved: false,
527
+ userId: editor.getOption(discussionPlugin, 'currentUserId'),
528
+ };
529
+
530
+ editor.setOption(discussionPlugin, 'discussions', [
531
+ ...discussions,
532
+ newDiscussion,
533
+ ]);
534
+
535
+ const id = newDiscussion.id;
536
+
537
+ commentsNodeEntry.forEach(([, path]) => {
538
+ editor.tf.setNodes(
539
+ {
540
+ [getCommentKey(id)]: true,
541
+ },
542
+ { at: path, split: true }
543
+ );
544
+ editor.tf.unsetNodes([getDraftCommentKey()], { at: path });
545
+ });
546
+ }, [commentValue, commentEditor.tf, discussionId, editor, discussions]);
547
+
548
+ return (
549
+ <div className={cn('flex w-full', className)}>
550
+ <div className="mt-2 mr-1 shrink-0">
551
+ {/* Replace to your own backend or refer to potion */}
552
+ <Avatar className="size-5">
553
+ <AvatarImage alt={userInfo?.name} src={userInfo?.avatarUrl} />
554
+ <AvatarFallback>{userInfo?.name?.[0]}</AvatarFallback>
555
+ </Avatar>
556
+ </div>
557
+
558
+ <div className="relative flex grow gap-2">
559
+ <Plate
560
+ onChange={({ value }) => {
561
+ setCommentValue(value);
562
+ }}
563
+ editor={commentEditor}
564
+ >
565
+ <EditorContainer variant="comment">
566
+ <Editor
567
+ variant="comment"
568
+ className="min-h-[25px] grow pt-0.5 pr-8"
569
+ onKeyDown={(e) => {
570
+ if (e.key === 'Enter' && !e.shiftKey) {
571
+ e.preventDefault();
572
+ onAddComment();
573
+ }
574
+ }}
575
+ placeholder="Reply..."
576
+ autoComplete="off"
577
+ autoFocus={autoFocus}
578
+ />
579
+
580
+ <Button
581
+ size="icon"
582
+ variant="ghost"
583
+ className="absolute right-0.5 bottom-0.5 ml-auto size-6 shrink-0"
584
+ disabled={commentContent.trim().length === 0}
585
+ onClick={(e) => {
586
+ e.stopPropagation();
587
+ onAddComment();
588
+ }}
589
+ >
590
+ <div className="flex size-6 items-center justify-center rounded-full">
591
+ <ArrowUpIcon />
592
+ </div>
593
+ </Button>
594
+ </EditorContainer>
595
+ </Plate>
596
+ </div>
597
+ </div>
598
+ );
599
+ }
600
+
601
+ export const formatCommentDate = (date: Date) => {
602
+ const now = new Date();
603
+ const diffMinutes = differenceInMinutes(now, date);
604
+ const diffHours = differenceInHours(now, date);
605
+ const diffDays = differenceInDays(now, date);
606
+
607
+ if (diffMinutes < 60) {
608
+ return `${diffMinutes}m`;
609
+ }
610
+ if (diffHours < 24) {
611
+ return `${diffHours}h`;
612
+ }
613
+ if (diffDays < 2) {
614
+ return `${diffDays}d`;
615
+ }
616
+
617
+ return format(date, 'MM/dd/yyyy');
618
+ };