The Appview for the kipclip.com atproto bookmarking service
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}