👁️
at dev 115 lines 3.1 kB view raw
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}