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} from "lucide-react";
45import { createCollection, updateCollection } from "../api/client";
46
47const EMOJI_OPTIONS = [
48 "📁",
49 "📚",
50 "💡",
51 "⭐",
52 "🔖",
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
80const ICON_OPTIONS = [
81 { icon: Folder, name: "folder" },
82 { icon: Star, name: "star" },
83 { icon: Heart, name: "heart" },
84 { icon: Bookmark, name: "bookmark" },
85 { icon: Lightbulb, name: "lightbulb" },
86 { icon: Zap, name: "zap" },
87 { icon: Coffee, name: "coffee" },
88 { icon: Music, name: "music" },
89 { icon: Camera, name: "camera" },
90 { icon: Code, name: "code" },
91 { icon: Globe, name: "globe" },
92 { icon: Flag, name: "flag" },
93 { icon: Tag, name: "tag" },
94 { icon: Box, name: "box" },
95 { icon: Archive, name: "archive" },
96 { icon: FileText, name: "file" },
97 { icon: Image, name: "image" },
98 { icon: Video, name: "video" },
99 { icon: Mail, name: "mail" },
100 { icon: MapPin, name: "pin" },
101 { icon: Calendar, name: "calendar" },
102 { icon: Clock, name: "clock" },
103 { icon: Search, name: "search" },
104 { icon: Settings, name: "settings" },
105 { icon: User, name: "user" },
106 { icon: Users, name: "users" },
107 { icon: Home, name: "home" },
108 { icon: Briefcase, name: "briefcase" },
109 { icon: Gift, name: "gift" },
110 { icon: Award, name: "award" },
111 { icon: Target, name: "target" },
112 { icon: TrendingUp, name: "trending" },
113 { icon: Activity, name: "activity" },
114 { icon: Cpu, name: "cpu" },
115 { icon: Database, name: "database" },
116 { icon: Cloud, name: "cloud" },
117 { icon: Sun, name: "sun" },
118 { icon: Moon, name: "moon" },
119 { icon: Flame, name: "flame" },
120 { icon: Leaf, name: "leaf" },
121];
122
123export default function CollectionModal({
124 isOpen,
125 onClose,
126 onSuccess,
127 collectionToEdit,
128}) {
129 const [name, setName] = useState("");
130 const [description, setDescription] = useState("");
131 const [icon, setIcon] = useState("");
132 const [customEmoji, setCustomEmoji] = useState("");
133 const [activeTab, setActiveTab] = useState("emoji");
134 const [loading, setLoading] = useState(false);
135 const [error, setError] = useState(null);
136
137 useEffect(() => {
138 if (collectionToEdit) {
139 setName(collectionToEdit.name);
140 setDescription(collectionToEdit.description || "");
141 const savedIcon = collectionToEdit.icon || "";
142 setIcon(savedIcon);
143 setCustomEmoji(savedIcon);
144
145 if (savedIcon.startsWith("icon:")) {
146 setActiveTab("icons");
147 }
148 } else {
149 setName("");
150 setDescription("");
151 setIcon("");
152 setCustomEmoji("");
153 }
154 setError(null);
155 }, [collectionToEdit, isOpen]);
156
157 if (!isOpen) return null;
158
159 const handleEmojiSelect = (emoji) => {
160 if (icon === emoji) {
161 setIcon("");
162 setCustomEmoji("");
163 } else {
164 setIcon(emoji);
165 setCustomEmoji(emoji);
166 }
167 };
168
169 const handleIconSelect = (iconName) => {
170 const value = `icon:${iconName}`;
171 if (icon === value) {
172 setIcon("");
173 setCustomEmoji("");
174 } else {
175 setIcon(value);
176 setCustomEmoji(value);
177 }
178 };
179
180 const handleCustomEmojiChange = (e) => {
181 const value = e.target.value;
182 setCustomEmoji(value);
183 const emojiMatch = value.match(
184 /(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)/gu,
185 );
186 if (emojiMatch && emojiMatch.length > 0) {
187 setIcon(emojiMatch[emojiMatch.length - 1]);
188 } else if (value === "") {
189 setIcon("");
190 }
191 };
192
193 const handleSubmit = async (e) => {
194 e.preventDefault();
195 setLoading(true);
196 setError(null);
197
198 try {
199 if (collectionToEdit) {
200 await updateCollection(collectionToEdit.uri, name, description, icon);
201 } else {
202 await createCollection(name, description, icon);
203 }
204 onSuccess();
205 onClose();
206 } catch (err) {
207 console.error(err);
208 setError(err.message || "Failed to save collection");
209 } finally {
210 setLoading(false);
211 }
212 };
213
214 return (
215 <div className="modal-overlay" onClick={onClose}>
216 <div
217 className="modal-container"
218 style={{ maxWidth: "420px" }}
219 onClick={(e) => e.stopPropagation()}
220 >
221 <div className="modal-header">
222 <h2 className="modal-title">
223 {collectionToEdit ? "Edit Collection" : "New Collection"}
224 </h2>
225 <button onClick={onClose} className="modal-close-btn">
226 <X size={20} />
227 </button>
228 </div>
229
230 <form onSubmit={handleSubmit} className="modal-form">
231 {error && (
232 <div
233 className="card text-error"
234 style={{
235 padding: "12px",
236 background: "rgba(239, 68, 68, 0.1)",
237 borderColor: "rgba(239, 68, 68, 0.2)",
238 fontSize: "0.9rem",
239 }}
240 >
241 {error}
242 </div>
243 )}
244
245 <div className="form-group">
246 <label className="form-label">Icon</label>
247 <div className="icon-picker-tabs">
248 <button
249 type="button"
250 className={`icon-picker-tab ${activeTab === "emoji" ? "active" : ""}`}
251 onClick={() => setActiveTab("emoji")}
252 >
253 Emoji
254 </button>
255 <button
256 type="button"
257 className={`icon-picker-tab ${activeTab === "icons" ? "active" : ""}`}
258 onClick={() => setActiveTab("icons")}
259 >
260 Icons
261 </button>
262 </div>
263
264 {activeTab === "emoji" && (
265 <div className="emoji-picker-wrapper">
266 <div className="emoji-custom-input">
267 <input
268 type="text"
269 value={customEmoji.startsWith("icon:") ? "" : customEmoji}
270 onChange={handleCustomEmojiChange}
271 placeholder="Type any emoji..."
272 className="form-input"
273 />
274 </div>
275 <div className="emoji-picker">
276 {EMOJI_OPTIONS.map((emoji) => (
277 <button
278 key={emoji}
279 type="button"
280 className={`emoji-option ${icon === emoji ? "selected" : ""}`}
281 onClick={() => handleEmojiSelect(emoji)}
282 >
283 {emoji}
284 </button>
285 ))}
286 </div>
287 </div>
288 )}
289
290 {activeTab === "icons" && (
291 <div className="icon-picker">
292 {ICON_OPTIONS.map(({ icon: IconComponent, name: iconName }) => (
293 <button
294 key={iconName}
295 type="button"
296 className={`icon-option ${icon === `icon:${iconName}` ? "selected" : ""}`}
297 onClick={() => handleIconSelect(iconName)}
298 >
299 <IconComponent size={20} />
300 </button>
301 ))}
302 </div>
303 )}
304 </div>
305
306 <div className="form-group">
307 <label className="form-label">Name</label>
308 <input
309 type="text"
310 value={name}
311 onChange={(e) => setName(e.target.value)}
312 required
313 className="form-input"
314 placeholder="My Favorites"
315 />
316 </div>
317
318 <div className="form-group">
319 <label className="form-label">Description</label>
320 <textarea
321 value={description}
322 onChange={(e) => setDescription(e.target.value)}
323 rows={2}
324 className="form-textarea"
325 placeholder="A collection of..."
326 />
327 </div>
328
329 <div className="modal-actions">
330 <button type="button" onClick={onClose} className="btn btn-ghost">
331 Cancel
332 </button>
333 <button
334 type="submit"
335 disabled={loading}
336 className="btn btn-primary"
337 style={loading ? { opacity: 0.7, cursor: "wait" } : {}}
338 >
339 {loading
340 ? "Saving..."
341 : collectionToEdit
342 ? "Save Changes"
343 : "Create Collection"}
344 </button>
345 </div>
346 </form>
347 </div>
348 </div>
349 );
350}