Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 8.4 kB view raw
1import { useState, useEffect, useCallback } from "react"; 2import { X, Plus, Check, Folder } from "lucide-react"; 3import { 4 getCollections, 5 addItemToCollection, 6 getCollectionsContaining, 7} from "../api/client"; 8import { useAuth } from "../context/AuthContext"; 9import CollectionModal from "./CollectionModal"; 10 11export default function AddToCollectionModal({ 12 isOpen, 13 onClose, 14 annotationUri, 15}) { 16 const { user } = useAuth(); 17 const [collections, setCollections] = useState([]); 18 const [loading, setLoading] = useState(true); 19 const [addingTo, setAddingTo] = useState(null); 20 const [addedTo, setAddedTo] = useState(new Set()); 21 const [createModalOpen, setCreateModalOpen] = useState(false); 22 const [error, setError] = useState(null); 23 24 useEffect(() => { 25 if (isOpen && user) { 26 if (!annotationUri) { 27 setLoading(false); 28 return; 29 } 30 loadCollections(); 31 setError(null); 32 } 33 }, [isOpen, user, annotationUri, loadCollections]); 34 35 const loadCollections = useCallback(async () => { 36 try { 37 setLoading(true); 38 const [data, existingURIs] = await Promise.all([ 39 getCollections(user?.did), 40 annotationUri ? getCollectionsContaining(annotationUri) : [], 41 ]); 42 43 const items = Array.isArray(data) ? data : data.items || []; 44 setCollections(items); 45 setAddedTo(new Set(existingURIs || [])); 46 } catch (err) { 47 console.error(err); 48 setError("Failed to load collections"); 49 } finally { 50 setLoading(false); 51 } 52 }, [user?.did, annotationUri]); 53 54 const handleAdd = async (collectionUri) => { 55 if (addedTo.has(collectionUri)) return; 56 57 try { 58 setAddingTo(collectionUri); 59 await addItemToCollection(collectionUri, annotationUri); 60 setAddedTo((prev) => new Set([...prev, collectionUri])); 61 } catch (err) { 62 console.error(err); 63 alert("Failed to add to collection"); 64 } finally { 65 setAddingTo(null); 66 } 67 }; 68 69 if (!isOpen) return null; 70 71 return ( 72 <> 73 <div className="modal-overlay" onClick={onClose}> 74 <div 75 className="modal-container" 76 style={{ 77 maxWidth: "380px", 78 maxHeight: "80dvh", 79 display: "flex", 80 flexDirection: "column", 81 }} 82 onClick={(e) => e.stopPropagation()} 83 > 84 <div className="modal-header"> 85 <h2 86 className="modal-title" 87 style={{ display: "flex", alignItems: "center", gap: "8px" }} 88 > 89 <Folder size={20} style={{ color: "var(--accent)" }} /> 90 Add to Collection 91 </h2> 92 <button onClick={onClose} className="modal-close-btn"> 93 <X size={20} /> 94 </button> 95 </div> 96 97 <div style={{ overflowY: "auto", padding: "8px", flex: 1 }}> 98 {loading ? ( 99 <div 100 style={{ 101 padding: "32px", 102 display: "flex", 103 alignItems: "center", 104 justifyContent: "center", 105 flexDirection: "column", 106 gap: "12px", 107 color: "var(--text-tertiary)", 108 }} 109 > 110 <div className="spinner"></div> 111 <span style={{ fontSize: "0.9rem" }}> 112 Loading collections... 113 </span> 114 </div> 115 ) : error ? ( 116 <div style={{ padding: "24px", textAlign: "center" }}> 117 <p 118 className="text-error" 119 style={{ fontSize: "0.9rem", marginBottom: "12px" }} 120 > 121 {error} 122 </p> 123 <button 124 onClick={loadCollections} 125 className="btn btn-secondary btn-sm" 126 > 127 Try Again 128 </button> 129 </div> 130 ) : collections.length === 0 ? ( 131 <div className="empty-state" style={{ padding: "32px" }}> 132 <div className="empty-state-icon"> 133 <Folder size={24} /> 134 </div> 135 <p className="empty-state-title" style={{ fontSize: "1rem" }}> 136 No collections found 137 </p> 138 <p className="empty-state-text"> 139 Create a collection to start organizing your items. 140 </p> 141 </div> 142 ) : ( 143 <div 144 style={{ display: "flex", flexDirection: "column", gap: "4px" }} 145 > 146 {collections.map((col) => { 147 const isAdded = addedTo.has(col.uri); 148 const isAdding = addingTo === col.uri; 149 150 return ( 151 <button 152 key={col.uri} 153 onClick={() => handleAdd(col.uri)} 154 disabled={isAdding || isAdded} 155 className="collection-list-item" 156 style={{ 157 opacity: isAdded ? 0.7 : 1, 158 cursor: isAdded ? "default" : "pointer", 159 }} 160 > 161 <div 162 style={{ 163 display: "flex", 164 flexDirection: "column", 165 minWidth: 0, 166 }} 167 > 168 <span 169 style={{ 170 fontWeight: 500, 171 overflow: "hidden", 172 textOverflow: "ellipsis", 173 whiteSpace: "nowrap", 174 }} 175 > 176 {col.name} 177 </span> 178 {col.description && ( 179 <span 180 style={{ 181 fontSize: "0.75rem", 182 color: "var(--text-tertiary)", 183 overflow: "hidden", 184 textOverflow: "ellipsis", 185 whiteSpace: "nowrap", 186 marginTop: "2px", 187 }} 188 > 189 {col.description} 190 </span> 191 )} 192 </div> 193 194 {isAdding ? ( 195 <span 196 className="spinner spinner-sm" 197 style={{ marginLeft: "12px" }} 198 /> 199 ) : isAdded ? ( 200 <Check 201 size={20} 202 style={{ 203 color: "var(--success)", 204 marginLeft: "12px", 205 }} 206 /> 207 ) : ( 208 <Plus 209 size={18} 210 style={{ 211 color: "var(--text-tertiary)", 212 opacity: 0, 213 marginLeft: "12px", 214 }} 215 className="collection-list-item-icon" 216 /> 217 )} 218 </button> 219 ); 220 })} 221 </div> 222 )} 223 </div> 224 225 <div 226 style={{ 227 padding: "16px", 228 borderTop: "1px solid var(--border)", 229 background: "var(--bg-tertiary)", 230 display: "flex", 231 gap: "8px", 232 }} 233 > 234 <button 235 onClick={() => setCreateModalOpen(true)} 236 className="btn btn-secondary" 237 style={{ flex: 1 }} 238 > 239 <Plus size={18} /> 240 New Collection 241 </button> 242 <button 243 onClick={onClose} 244 className="btn btn-primary" 245 style={{ flex: 1 }} 246 > 247 Done 248 </button> 249 </div> 250 </div> 251 </div> 252 253 <CollectionModal 254 isOpen={createModalOpen} 255 onClose={() => setCreateModalOpen(false)} 256 onSuccess={() => { 257 loadCollections(); 258 }} 259 /> 260 </> 261 ); 262}