(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 396 lines 10 kB view raw
1import { useState, useEffect } from "react"; 2import { 3 X, 4 Folder, 5 Star, 6 Heart, 7 Bookmark, 8 Lightbulb, 9 Zap, 10 Coffee, 11 Music, 12 Camera, 13 Code, 14 Globe, 15 Flag, 16 Tag, 17 Box, 18 Archive, 19 FileText, 20 Image, 21 Video, 22 Mail, 23 MapPin, 24 Calendar, 25 Clock, 26 Search, 27 Settings, 28 User, 29 Users, 30 Home, 31 Briefcase, 32 Gift, 33 Award, 34 Target, 35 TrendingUp, 36 Activity, 37 Cpu, 38 Database, 39 Cloud, 40 Sun, 41 Moon, 42 Flame, 43 Leaf, 44 Trash2, 45} from "lucide-react"; 46import { 47 createCollection, 48 updateCollection, 49 deleteCollection, 50} from "../api/client"; 51 52const EMOJI_OPTIONS = [ 53 "📁", 54 "📚", 55 "💡", 56 "⭐", 57 "🔖", 58 "💻", 59 "🎨", 60 "📝", 61 "🔬", 62 "🎯", 63 "🚀", 64 "💎", 65 "🌟", 66 "📌", 67 "💼", 68 "🎮", 69 "🎵", 70 "🎬", 71 "❤️", 72 "🔥", 73 "🌈", 74 "🌸", 75 "🌿", 76 "🧠", 77 "🏆", 78 "📊", 79 "🎓", 80 "✨", 81 "🔧", 82 "⚡", 83]; 84 85const ICON_OPTIONS = [ 86 { icon: Folder, name: "folder" }, 87 { icon: Star, name: "star" }, 88 { icon: Heart, name: "heart" }, 89 { icon: Bookmark, name: "bookmark" }, 90 { icon: Lightbulb, name: "lightbulb" }, 91 { icon: Zap, name: "zap" }, 92 { icon: Coffee, name: "coffee" }, 93 { icon: Music, name: "music" }, 94 { icon: Camera, name: "camera" }, 95 { icon: Code, name: "code" }, 96 { icon: Globe, name: "globe" }, 97 { icon: Flag, name: "flag" }, 98 { icon: Tag, name: "tag" }, 99 { icon: Box, name: "box" }, 100 { icon: Archive, name: "archive" }, 101 { icon: FileText, name: "file" }, 102 { icon: Image, name: "image" }, 103 { icon: Video, name: "video" }, 104 { icon: Mail, name: "mail" }, 105 { icon: MapPin, name: "pin" }, 106 { icon: Calendar, name: "calendar" }, 107 { icon: Clock, name: "clock" }, 108 { icon: Search, name: "search" }, 109 { icon: Settings, name: "settings" }, 110 { icon: User, name: "user" }, 111 { icon: Users, name: "users" }, 112 { icon: Home, name: "home" }, 113 { icon: Briefcase, name: "briefcase" }, 114 { icon: Gift, name: "gift" }, 115 { icon: Award, name: "award" }, 116 { icon: Target, name: "target" }, 117 { icon: TrendingUp, name: "trending" }, 118 { icon: Activity, name: "activity" }, 119 { icon: Cpu, name: "cpu" }, 120 { icon: Database, name: "database" }, 121 { icon: Cloud, name: "cloud" }, 122 { icon: Sun, name: "sun" }, 123 { icon: Moon, name: "moon" }, 124 { icon: Flame, name: "flame" }, 125 { icon: Leaf, name: "leaf" }, 126]; 127 128export default function CollectionModal({ 129 isOpen, 130 onClose, 131 onSuccess, 132 collectionToEdit, 133 onDelete, 134}) { 135 const [name, setName] = useState(""); 136 const [description, setDescription] = useState(""); 137 const [icon, setIcon] = useState(""); 138 const [customEmoji, setCustomEmoji] = useState(""); 139 const [activeTab, setActiveTab] = useState("emoji"); 140 const [loading, setLoading] = useState(false); 141 const [deleting, setDeleting] = useState(false); 142 const [error, setError] = useState(null); 143 144 useEffect(() => { 145 if (collectionToEdit) { 146 setName(collectionToEdit.name); 147 setDescription(collectionToEdit.description || ""); 148 const savedIcon = collectionToEdit.icon || ""; 149 setIcon(savedIcon); 150 setCustomEmoji(savedIcon); 151 152 if (savedIcon.startsWith("icon:")) { 153 setActiveTab("icons"); 154 } 155 } else { 156 setName(""); 157 setDescription(""); 158 setIcon(""); 159 setCustomEmoji(""); 160 } 161 setError(null); 162 }, [collectionToEdit, isOpen]); 163 164 if (!isOpen) return null; 165 166 const handleEmojiSelect = (emoji) => { 167 if (icon === emoji) { 168 setIcon(""); 169 setCustomEmoji(""); 170 } else { 171 setIcon(emoji); 172 setCustomEmoji(emoji); 173 } 174 }; 175 176 const handleIconSelect = (iconName) => { 177 const value = `icon:${iconName}`; 178 if (icon === value) { 179 setIcon(""); 180 setCustomEmoji(""); 181 } else { 182 setIcon(value); 183 setCustomEmoji(value); 184 } 185 }; 186 187 const handleCustomEmojiChange = (e) => { 188 const value = e.target.value; 189 setCustomEmoji(value); 190 const emojiMatch = value.match( 191 /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)/gu, 192 ); 193 if (emojiMatch && emojiMatch.length > 0) { 194 setIcon(emojiMatch[emojiMatch.length - 1]); 195 } else if (value === "") { 196 setIcon(""); 197 } 198 }; 199 200 const handleSubmit = async (e) => { 201 e.preventDefault(); 202 setLoading(true); 203 setError(null); 204 205 try { 206 if (collectionToEdit) { 207 await updateCollection(collectionToEdit.uri, name, description, icon); 208 } else { 209 await createCollection(name, description, icon); 210 } 211 onSuccess(); 212 onClose(); 213 } catch (err) { 214 console.error(err); 215 setError(err.message || "Failed to save collection"); 216 } finally { 217 setLoading(false); 218 } 219 }; 220 221 const handleDelete = async () => { 222 if ( 223 !confirm( 224 "Delete this collection and all its items? This cannot be undone.", 225 ) 226 ) { 227 return; 228 } 229 setDeleting(true); 230 setError(null); 231 232 try { 233 await deleteCollection(collectionToEdit.uri); 234 if (onDelete) { 235 onDelete(); 236 } else { 237 onSuccess(); 238 } 239 onClose(); 240 } catch (err) { 241 console.error(err); 242 setError(err.message || "Failed to delete collection"); 243 } finally { 244 setDeleting(false); 245 } 246 }; 247 248 return ( 249 <div className="modal-overlay" onClick={onClose}> 250 <div 251 className="modal-container" 252 style={{ maxWidth: "420px" }} 253 onClick={(e) => e.stopPropagation()} 254 > 255 <div className="modal-header"> 256 <h2 className="modal-title"> 257 {collectionToEdit ? "Edit Collection" : "New Collection"} 258 </h2> 259 <button onClick={onClose} className="modal-close-btn"> 260 <X size={20} /> 261 </button> 262 </div> 263 264 <form onSubmit={handleSubmit} className="modal-form"> 265 {error && ( 266 <div 267 className="card text-error" 268 style={{ 269 padding: "12px", 270 background: "rgba(239, 68, 68, 0.1)", 271 borderColor: "rgba(239, 68, 68, 0.2)", 272 fontSize: "0.9rem", 273 }} 274 > 275 {error} 276 </div> 277 )} 278 279 <div className="form-group"> 280 <label className="form-label">Icon</label> 281 <div className="icon-picker-tabs"> 282 <button 283 type="button" 284 className={`icon-picker-tab ${activeTab === "emoji" ? "active" : ""}`} 285 onClick={() => setActiveTab("emoji")} 286 > 287 Emoji 288 </button> 289 <button 290 type="button" 291 className={`icon-picker-tab ${activeTab === "icons" ? "active" : ""}`} 292 onClick={() => setActiveTab("icons")} 293 > 294 Icons 295 </button> 296 </div> 297 298 {activeTab === "emoji" && ( 299 <div className="emoji-picker-wrapper"> 300 <div className="emoji-custom-input"> 301 <input 302 type="text" 303 value={customEmoji.startsWith("icon:") ? "" : customEmoji} 304 onChange={handleCustomEmojiChange} 305 placeholder="Type any emoji..." 306 className="form-input" 307 /> 308 </div> 309 <div className="emoji-picker"> 310 {EMOJI_OPTIONS.map((emoji) => ( 311 <button 312 key={emoji} 313 type="button" 314 className={`emoji-option ${icon === emoji ? "selected" : ""}`} 315 onClick={() => handleEmojiSelect(emoji)} 316 > 317 {emoji} 318 </button> 319 ))} 320 </div> 321 </div> 322 )} 323 324 {activeTab === "icons" && ( 325 <div className="icon-picker"> 326 {ICON_OPTIONS.map(({ icon: IconComponent, name: iconName }) => ( 327 <button 328 key={iconName} 329 type="button" 330 className={`icon-option ${icon === `icon:${iconName}` ? "selected" : ""}`} 331 onClick={() => handleIconSelect(iconName)} 332 > 333 <IconComponent size={20} /> 334 </button> 335 ))} 336 </div> 337 )} 338 </div> 339 340 <div className="form-group"> 341 <label className="form-label">Name</label> 342 <input 343 type="text" 344 value={name} 345 onChange={(e) => setName(e.target.value)} 346 required 347 className="form-input" 348 placeholder="My Favorites" 349 /> 350 </div> 351 352 <div className="form-group"> 353 <label className="form-label">Description</label> 354 <textarea 355 value={description} 356 onChange={(e) => setDescription(e.target.value)} 357 rows={2} 358 className="form-textarea" 359 placeholder="A collection of..." 360 /> 361 </div> 362 363 <div className="modal-actions"> 364 {collectionToEdit && ( 365 <button 366 type="button" 367 onClick={handleDelete} 368 disabled={deleting} 369 className="btn btn-danger" 370 > 371 <Trash2 size={16} /> 372 {deleting ? "Deleting..." : "Delete"} 373 </button> 374 )} 375 <div style={{ flex: 1 }} /> 376 <button type="button" onClick={onClose} className="btn btn-ghost"> 377 Cancel 378 </button> 379 <button 380 type="submit" 381 disabled={loading} 382 className="btn btn-primary" 383 style={loading ? { opacity: 0.7, cursor: "wait" } : {}} 384 > 385 {loading 386 ? "Saving..." 387 : collectionToEdit 388 ? "Save Changes" 389 : "Create Collection"} 390 </button> 391 </div> 392 </form> 393 </div> 394 </div> 395 ); 396}