(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

at ui-refactor 184 lines 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}