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">"{highlightedText}"</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}