(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 262 lines 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 const loadCollections = useCallback(async () => { 25 try { 26 setLoading(true); 27 const [data, existingURIs] = await Promise.all([ 28 getCollections(user?.did), 29 annotationUri ? getCollectionsContaining(annotationUri) : [], 30 ]); 31 32 const items = Array.isArray(data) ? data : data.items || []; 33 setCollections(items); 34 setAddedTo(new Set(existingURIs || [])); 35 } catch (err) { 36 console.error(err); 37 setError("Failed to load collections"); 38 } finally { 39 setLoading(false); 40 } 41 }, [user?.did, annotationUri]); 42 43 useEffect(() => { 44 if (isOpen && user) { 45 if (!annotationUri) { 46 setLoading(false); 47 return; 48 } 49 loadCollections(); 50 setError(null); 51 } 52 }, [isOpen, user, annotationUri, loadCollections]); 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}