Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 4.9 kB view raw
1import { useState } from "react"; 2import { createAnnotation, createHighlight } from "../api/client"; 3 4export default function Composer({ 5 url, 6 selector: initialSelector, 7 onSuccess, 8 onCancel, 9}) { 10 const [text, setText] = useState(""); 11 const [quoteText, setQuoteText] = useState(""); 12 const [tags, setTags] = useState(""); 13 const [selector, setSelector] = useState(initialSelector); 14 const [loading, setLoading] = useState(false); 15 const [error, setError] = useState(null); 16 const [showQuoteInput, setShowQuoteInput] = useState(false); 17 18 const highlightedText = 19 selector?.type === "TextQuoteSelector" ? selector.exact : null; 20 21 const handleSubmit = async (e) => { 22 e.preventDefault(); 23 if (!text.trim() && !highlightedText && !quoteText.trim()) return; 24 25 try { 26 setLoading(true); 27 setError(null); 28 29 let finalSelector = selector; 30 if (!finalSelector && quoteText.trim()) { 31 finalSelector = { 32 type: "TextQuoteSelector", 33 exact: quoteText.trim(), 34 }; 35 } 36 37 const tagList = tags 38 .split(",") 39 .map((t) => t.trim()) 40 .filter(Boolean); 41 42 if (!text.trim()) { 43 await createHighlight({ 44 url, 45 selector: finalSelector, 46 color: "yellow", 47 tags: tagList, 48 }); 49 } else { 50 await createAnnotation({ 51 url, 52 text, 53 selector: finalSelector || undefined, 54 tags: tagList, 55 }); 56 } 57 58 setText(""); 59 setQuoteText(""); 60 setSelector(null); 61 if (onSuccess) onSuccess(); 62 } catch (err) { 63 setError(err.message); 64 } finally { 65 setLoading(false); 66 } 67 }; 68 69 const handleRemoveSelector = () => { 70 setSelector(null); 71 setQuoteText(""); 72 setShowQuoteInput(false); 73 }; 74 75 return ( 76 <form onSubmit={handleSubmit} className="composer"> 77 <div className="composer-header"> 78 <h3 className="composer-title">New Annotation</h3> 79 {url && <div className="composer-url">{url}</div>} 80 </div> 81 82 {} 83 {highlightedText && ( 84 <div className="composer-quote"> 85 <button 86 type="button" 87 className="composer-quote-remove" 88 onClick={handleRemoveSelector} 89 title="Remove selection" 90 > 91 × 92 </button> 93 <blockquote> 94 <mark className="quote-exact">&quot;{highlightedText}&quot;</mark> 95 </blockquote> 96 </div> 97 )} 98 99 {} 100 {!highlightedText && ( 101 <> 102 {!showQuoteInput ? ( 103 <button 104 type="button" 105 className="composer-add-quote" 106 onClick={() => setShowQuoteInput(true)} 107 > 108 + Add a quote from the page 109 </button> 110 ) : ( 111 <div className="composer-quote-input-wrapper"> 112 <textarea 113 value={quoteText} 114 onChange={(e) => setQuoteText(e.target.value)} 115 placeholder="Paste or type the text you're annotating..." 116 className="composer-quote-input" 117 rows={2} 118 /> 119 <button 120 type="button" 121 className="composer-quote-remove-btn" 122 onClick={handleRemoveSelector} 123 > 124 Remove 125 </button> 126 </div> 127 )} 128 </> 129 )} 130 131 <textarea 132 value={text} 133 onChange={(e) => setText(e.target.value)} 134 placeholder={ 135 highlightedText || quoteText 136 ? "Add your comment about this selection..." 137 : "Write your annotation..." 138 } 139 className="composer-input" 140 rows={4} 141 maxLength={3000} 142 disabled={loading} 143 /> 144 145 <div className="composer-tags"> 146 <input 147 type="text" 148 value={tags} 149 onChange={(e) => setTags(e.target.value)} 150 placeholder="Add tags (comma separated)..." 151 className="composer-tags-input" 152 disabled={loading} 153 /> 154 </div> 155 156 <div className="composer-footer"> 157 <span className="composer-count">{text.length}/3000</span> 158 <div className="composer-actions"> 159 {onCancel && ( 160 <button 161 type="button" 162 className="btn btn-ghost" 163 onClick={onCancel} 164 disabled={loading} 165 > 166 Cancel 167 </button> 168 )} 169 <button 170 type="submit" 171 className="btn btn-primary" 172 disabled={ 173 loading || (!text.trim() && !highlightedText && !quoteText) 174 } 175 > 176 {loading ? "Posting..." : "Post"} 177 </button> 178 </div> 179 </div> 180 181 {error && <div className="composer-error">{error}</div>} 182 </form> 183 ); 184}