Explore the margin.at codebase, lexicons, and more! margin.at
at v0.1.11 9.1 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 Lock, 16 Flag, 17 Tag, 18 Box, 19 Archive, 20 FileText, 21 Image, 22 Video, 23 Mail, 24 Phone, 25 MapPin, 26 Calendar, 27 Clock, 28 Search, 29 Settings, 30 User, 31 Users, 32 Home, 33 Briefcase, 34 ShoppingBag, 35 Gift, 36 Award, 37 Target, 38 TrendingUp, 39 BarChart, 40 PieChart, 41 Activity, 42 Cpu, 43 Database, 44 Cloud, 45 Sun, 46 Moon, 47 Flame, 48 Leaf, 49 Droplet, 50 Snowflake, 51} from "lucide-react"; 52import { createCollection, updateCollection } from "../api/client"; 53 54const EMOJI_OPTIONS = [ 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 "⚡", 85]; 86 87const ICON_OPTIONS = [ 88 { icon: Folder, name: "folder" }, 89 { icon: Star, name: "star" }, 90 { icon: Heart, name: "heart" }, 91 { icon: Bookmark, name: "bookmark" }, 92 { icon: Lightbulb, name: "lightbulb" }, 93 { icon: Zap, name: "zap" }, 94 { icon: Coffee, name: "coffee" }, 95 { icon: Music, name: "music" }, 96 { icon: Camera, name: "camera" }, 97 { icon: Code, name: "code" }, 98 { icon: Globe, name: "globe" }, 99 { icon: Flag, name: "flag" }, 100 { icon: Tag, name: "tag" }, 101 { icon: Box, name: "box" }, 102 { icon: Archive, name: "archive" }, 103 { icon: FileText, name: "file" }, 104 { icon: Image, name: "image" }, 105 { icon: Video, name: "video" }, 106 { icon: Mail, name: "mail" }, 107 { icon: MapPin, name: "pin" }, 108 { icon: Calendar, name: "calendar" }, 109 { icon: Clock, name: "clock" }, 110 { icon: Search, name: "search" }, 111 { icon: Settings, name: "settings" }, 112 { icon: User, name: "user" }, 113 { icon: Users, name: "users" }, 114 { icon: Home, name: "home" }, 115 { icon: Briefcase, name: "briefcase" }, 116 { icon: Gift, name: "gift" }, 117 { icon: Award, name: "award" }, 118 { icon: Target, name: "target" }, 119 { icon: TrendingUp, name: "trending" }, 120 { icon: Activity, name: "activity" }, 121 { icon: Cpu, name: "cpu" }, 122 { icon: Database, name: "database" }, 123 { icon: Cloud, name: "cloud" }, 124 { icon: Sun, name: "sun" }, 125 { icon: Moon, name: "moon" }, 126 { icon: Flame, name: "flame" }, 127 { icon: Leaf, name: "leaf" }, 128]; 129 130export default function CollectionModal({ 131 isOpen, 132 onClose, 133 onSuccess, 134 collectionToEdit, 135}) { 136 const [name, setName] = useState(""); 137 const [description, setDescription] = useState(""); 138 const [icon, setIcon] = useState(""); 139 const [customEmoji, setCustomEmoji] = useState(""); 140 const [activeTab, setActiveTab] = useState("emoji"); 141 const [loading, setLoading] = 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 return ( 222 <div className="modal-overlay" onClick={onClose}> 223 <div 224 className="modal-container" 225 style={{ maxWidth: "420px" }} 226 onClick={(e) => e.stopPropagation()} 227 > 228 <div className="modal-header"> 229 <h2 className="modal-title"> 230 {collectionToEdit ? "Edit Collection" : "New Collection"} 231 </h2> 232 <button onClick={onClose} className="modal-close-btn"> 233 <X size={20} /> 234 </button> 235 </div> 236 237 <form onSubmit={handleSubmit} className="modal-form"> 238 {error && ( 239 <div 240 className="card text-error" 241 style={{ 242 padding: "12px", 243 background: "rgba(239, 68, 68, 0.1)", 244 borderColor: "rgba(239, 68, 68, 0.2)", 245 fontSize: "0.9rem", 246 }} 247 > 248 {error} 249 </div> 250 )} 251 252 <div className="form-group"> 253 <label className="form-label">Icon</label> 254 <div className="icon-picker-tabs"> 255 <button 256 type="button" 257 className={`icon-picker-tab ${activeTab === "emoji" ? "active" : ""}`} 258 onClick={() => setActiveTab("emoji")} 259 > 260 Emoji 261 </button> 262 <button 263 type="button" 264 className={`icon-picker-tab ${activeTab === "icons" ? "active" : ""}`} 265 onClick={() => setActiveTab("icons")} 266 > 267 Icons 268 </button> 269 </div> 270 271 {activeTab === "emoji" && ( 272 <div className="emoji-picker-wrapper"> 273 <div className="emoji-custom-input"> 274 <input 275 type="text" 276 value={customEmoji.startsWith("icon:") ? "" : customEmoji} 277 onChange={handleCustomEmojiChange} 278 placeholder="Type any emoji..." 279 className="form-input" 280 /> 281 </div> 282 <div className="emoji-picker"> 283 {EMOJI_OPTIONS.map((emoji) => ( 284 <button 285 key={emoji} 286 type="button" 287 className={`emoji-option ${icon === emoji ? "selected" : ""}`} 288 onClick={() => handleEmojiSelect(emoji)} 289 > 290 {emoji} 291 </button> 292 ))} 293 </div> 294 </div> 295 )} 296 297 {activeTab === "icons" && ( 298 <div className="icon-picker"> 299 {ICON_OPTIONS.map(({ icon: IconComponent, name: iconName }) => ( 300 <button 301 key={iconName} 302 type="button" 303 className={`icon-option ${icon === `icon:${iconName}` ? "selected" : ""}`} 304 onClick={() => handleIconSelect(iconName)} 305 > 306 <IconComponent size={20} /> 307 </button> 308 ))} 309 </div> 310 )} 311 </div> 312 313 <div className="form-group"> 314 <label className="form-label">Name</label> 315 <input 316 type="text" 317 value={name} 318 onChange={(e) => setName(e.target.value)} 319 required 320 className="form-input" 321 placeholder="My Favorites" 322 /> 323 </div> 324 325 <div className="form-group"> 326 <label className="form-label">Description</label> 327 <textarea 328 value={description} 329 onChange={(e) => setDescription(e.target.value)} 330 rows={2} 331 className="form-textarea" 332 placeholder="A collection of..." 333 /> 334 </div> 335 336 <div className="modal-actions"> 337 <button type="button" onClick={onClose} className="btn btn-ghost"> 338 Cancel 339 </button> 340 <button 341 type="submit" 342 disabled={loading} 343 className="btn btn-primary" 344 style={loading ? { opacity: 0.7, cursor: "wait" } : {}} 345 > 346 {loading 347 ? "Saving..." 348 : collectionToEdit 349 ? "Save Changes" 350 : "Create Collection"} 351 </button> 352 </div> 353 </form> 354 </div> 355 </div> 356 ); 357}