👁️
1import { useCallback, useMemo, useState } from "react";
2import { ProseMirrorEditor } from "@/components/richtext/ProseMirrorEditor";
3import { schema } from "@/components/richtext/schema";
4import type { Document } from "@/lib/lexicons/types/com/deckbelcher/richtext";
5import { lexiconToTree, treeToLexicon } from "@/lib/richtext-convert";
6import { useProseMirror } from "@/lib/useProseMirror";
7
8interface CommentFormProps {
9 initialContent?: Document;
10 onSubmit: (content: Document) => void;
11 onCancel?: () => void;
12 isPending?: boolean;
13 placeholder?: string;
14 submitLabel?: string;
15 availableTags?: string[];
16}
17
18export function CommentForm({
19 initialContent,
20 onSubmit,
21 onCancel,
22 isPending,
23 placeholder = "Write a comment...",
24 submitLabel,
25 availableTags,
26}: CommentFormProps) {
27 const isEditMode = !!initialContent;
28 const [key, setKey] = useState(0);
29
30 const initialPMDoc = useMemo(() => {
31 if (!initialContent) return undefined;
32 return lexiconToTree(initialContent).toJSON();
33 }, [initialContent]);
34
35 const { doc, onChange, isDirty } = useProseMirror({
36 initialDoc: initialPMDoc,
37 });
38
39 const hasContent = doc.textContent.trim().length > 0;
40
41 const handleSubmit = useCallback(() => {
42 if (!hasContent || isPending) return;
43 if (isEditMode && !isDirty) {
44 onSubmit(initialContent);
45 return;
46 }
47 const content = treeToLexicon(doc);
48 onSubmit(content);
49 if (!isEditMode) {
50 setKey((k) => k + 1);
51 }
52 }, [
53 doc,
54 hasContent,
55 isDirty,
56 initialContent,
57 isEditMode,
58 isPending,
59 onSubmit,
60 ]);
61
62 const label = submitLabel ?? (isEditMode ? "Done" : "Post");
63
64 return (
65 <div className="space-y-1">
66 <ProseMirrorEditor
67 key={key}
68 defaultValue={
69 isEditMode
70 ? doc
71 : schema.node("doc", null, [schema.node("paragraph")])
72 }
73 onChange={onChange}
74 placeholder={placeholder}
75 showToolbar
76 availableTags={availableTags}
77 className="text-sm"
78 />
79 <div className="flex items-center justify-end gap-2">
80 {isPending && (
81 <span className="text-sm text-gray-500 dark:text-zinc-300">
82 Saving...
83 </span>
84 )}
85 {!isPending && isEditMode && isDirty && (
86 <span className="text-sm text-gray-500 dark:text-zinc-300">
87 Unsaved changes
88 </span>
89 )}
90 {onCancel && (
91 <button
92 type="button"
93 onClick={onCancel}
94 disabled={isPending}
95 className="px-3 py-1.5 text-sm font-medium rounded-md bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-zinc-300 disabled:opacity-50"
96 >
97 Cancel
98 </button>
99 )}
100 <button
101 type="button"
102 onClick={handleSubmit}
103 disabled={!hasContent || isPending}
104 className={`px-3 py-1.5 text-sm font-medium rounded-md disabled:opacity-50 disabled:cursor-not-allowed ${
105 isEditMode
106 ? "bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-zinc-300"
107 : "bg-blue-600 hover:bg-blue-700 text-white"
108 }`}
109 >
110 {isPending ? "Saving..." : label}
111 </button>
112 </div>
113 </div>
114 );
115}