The Appview for the kipclip.com atproto bookmarking service
at main 119 lines 3.6 kB view raw
1import { useState } from "react"; 2import type { EnrichedTag } from "../../shared/types.ts"; 3 4interface AddTagProps { 5 onClose: () => void; 6 onTagAdded: (tag: EnrichedTag) => void; 7} 8 9export function AddTag({ onClose, onTagAdded }: AddTagProps) { 10 const [value, setValue] = useState(""); 11 const [loading, setLoading] = useState(false); 12 const [error, setError] = useState<string | null>(null); 13 14 async function handleSubmit(e: React.FormEvent) { 15 e.preventDefault(); 16 if (!value.trim()) return; 17 18 setLoading(true); 19 setError(null); 20 21 try { 22 const response = await fetch("/api/tags", { 23 method: "POST", 24 headers: { 25 "Content-Type": "application/json", 26 }, 27 body: JSON.stringify({ value: value.trim() }), 28 }); 29 30 if (!response.ok) { 31 const data = await response.json(); 32 throw new Error(data.error || "Failed to add tag"); 33 } 34 35 const data = await response.json(); 36 onTagAdded(data.tag); 37 } catch (err: any) { 38 setError(err.message); 39 setLoading(false); 40 } 41 } 42 43 return ( 44 <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> 45 <div className="bg-white rounded-lg max-w-md w-full p-6 fade-in"> 46 <div className="flex items-center justify-between mb-4"> 47 <h3 className="text-xl font-bold text-gray-800">Create Tag</h3> 48 <button 49 type="button" 50 onClick={onClose} 51 className="text-gray-400 hover:text-gray-600 text-2xl" 52 disabled={loading} 53 > 54 × 55 </button> 56 </div> 57 58 <form onSubmit={handleSubmit} className="space-y-4"> 59 <div> 60 <label 61 htmlFor="tag-value" 62 className="block text-sm font-medium text-gray-700 mb-2" 63 > 64 Tag Name 65 </label> 66 <input 67 type="text" 68 id="tag-value" 69 value={value} 70 onChange={(e) => setValue(e.target.value)} 71 placeholder="e.g., work, personal, reading" 72 maxLength={64} 73 className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-coral focus:border-transparent outline-none transition" 74 disabled={loading} 75 autoFocus 76 required 77 /> 78 <p className="text-xs text-gray-500 mt-1"> 79 {value.length}/64 characters 80 </p> 81 </div> 82 83 {error && ( 84 <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg text-sm"> 85 {error} 86 </div> 87 )} 88 89 <div className="flex gap-3"> 90 <button 91 type="button" 92 onClick={onClose} 93 className="flex-1 px-4 py-3 rounded-lg border border-gray-300 text-gray-700 font-medium hover:bg-gray-50 transition" 94 disabled={loading} 95 > 96 Cancel 97 </button> 98 <button 99 type="submit" 100 className="flex-1 btn-primary disabled:opacity-50" 101 disabled={loading || !value.trim()} 102 > 103 {loading 104 ? ( 105 <span className="flex items-center justify-center gap-2"> 106 <div className="spinner w-5 h-5 border-2"></div> 107 Creating... 108 </span> 109 ) 110 : ( 111 "Create Tag" 112 )} 113 </button> 114 </div> 115 </form> 116 </div> 117 </div> 118 ); 119}