A social knowledge tool for researchers built on ATProto

feat: add form validation and error handling for annotation creation

+55 -36
+55 -36
src/webapp/app/(authenticated)/annotations/create/page.tsx
··· 8 8 import { Button } from "@/components/ui/button"; 9 9 import { Input } from "@/components/ui/input"; 10 10 import { Label } from "@/components/ui/label"; 11 - import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; 12 - import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; 11 + import { 12 + Card, 13 + CardContent, 14 + CardDescription, 15 + CardHeader, 16 + CardTitle, 17 + } from "@/components/ui/card"; 18 + import { 19 + Select, 20 + SelectContent, 21 + SelectItem, 22 + SelectTrigger, 23 + SelectValue, 24 + } from "@/components/ui/select"; 13 25 import { Loader2 } from "lucide-react"; 14 26 import { AnnotationFieldInput } from "@/components/annotations/AnnotationFieldInput"; 15 27 16 28 export default function CreateAnnotationPage() { 17 29 const { accessToken } = useAuth(); 18 30 const router = useRouter(); 19 - 31 + 20 32 const [url, setUrl] = useState(""); 21 33 const [selectedTemplateId, setSelectedTemplateId] = useState(""); 22 34 const [templates, setTemplates] = useState([]); ··· 32 44 const fetchTemplates = async () => { 33 45 try { 34 46 if (!accessToken) return; 35 - 47 + 36 48 const templatesData = await annotationService.getTemplates(accessToken); 37 49 setTemplates(templatesData); 38 50 setLoadingTemplates(false); ··· 50 62 useEffect(() => { 51 63 const fetchTemplateDetails = async () => { 52 64 if (!selectedTemplateId || !accessToken) return; 53 - 65 + 54 66 try { 55 67 setLoading(true); 56 68 const templateData = await annotationService.getTemplateById( 57 69 accessToken, 58 70 selectedTemplateId 59 71 ); 60 - 72 + 61 73 setSelectedTemplate(templateData); 62 - 74 + 63 75 // Initialize form values for each field 64 76 const initialValues = {}; 65 - templateData.fields.forEach(field => { 77 + templateData.fields.forEach((field) => { 66 78 initialValues[field.id] = ""; 67 79 }); 68 - 80 + 69 81 setFormValues(initialValues); 70 82 setLoading(false); 71 83 } catch (error) { ··· 79 91 }, [selectedTemplateId, accessToken]); 80 92 81 93 const handleInputChange = (fieldId, value) => { 82 - setFormValues(prev => ({ 94 + setFormValues((prev) => ({ 83 95 ...prev, 84 - [fieldId]: value 96 + [fieldId]: value, 85 97 })); 86 98 }; 87 99 88 100 const handleSubmit = async (e) => { 89 101 e.preventDefault(); 90 - 102 + 91 103 if (!url) { 92 104 setError("URL is required"); 93 105 return; 94 106 } 95 - 107 + 96 108 if (!selectedTemplateId) { 97 109 setError("Please select a template"); 98 110 return; 99 111 } 100 - 112 + 101 113 try { 102 114 setLoading(true); 103 115 setError(""); 104 - 116 + 105 117 // Prepare annotations data 106 118 const annotations = []; 107 - 119 + 108 120 for (const field of selectedTemplate.fields) { 109 121 const value = formValues[field.id]; 110 - 122 + 111 123 if (field.required && !value) { 112 124 setError(`${field.name} is required`); 113 125 setLoading(false); 114 126 return; 115 127 } 116 - 128 + 117 129 // Format the value based on field type 118 - const formattedValue = formatAnnotationValue(field.definitionType, value); 119 - 130 + const formattedValue = formatAnnotationValue( 131 + field.definitionType, 132 + value 133 + ); 134 + 120 135 annotations.push({ 121 136 annotationFieldId: field.id, 122 137 type: field.definitionType, 123 - value: formattedValue 138 + value: formattedValue, 124 139 }); 125 140 } 126 - 141 + 127 142 // Submit annotations 128 143 const result = await annotationService.createAnnotationsFromTemplate( 129 144 accessToken, 130 145 { 131 146 url, 132 147 templateId: selectedTemplateId, 133 - annotations 148 + annotations, 134 149 } 135 150 ); 136 - 151 + 137 152 setSuccess(true); 138 153 setLoading(false); 139 - 154 + 140 155 // Redirect after a short delay 141 156 setTimeout(() => { 142 157 router.push("/annotations"); 143 158 }, 2000); 144 - 145 159 } catch (error) { 146 160 console.error("Error creating annotations:", error); 147 - setError(error.message || "Failed to create annotations. Please try again."); 161 + setError( 162 + error.message || "Failed to create annotations. Please try again." 163 + ); 148 164 setLoading(false); 149 165 } 150 166 }; ··· 152 168 return ( 153 169 <div className="container mx-auto py-6"> 154 170 <h1 className="text-3xl font-bold mb-6">Create Annotation</h1> 155 - 171 + 156 172 {error && ( 157 173 <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-6"> 158 174 {error} 159 175 </div> 160 176 )} 161 - 177 + 162 178 {success && ( 163 179 <div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded mb-6"> 164 180 Your annotation has been created successfully. Redirecting... 165 181 </div> 166 182 )} 167 - 183 + 168 184 <Card> 169 185 <CardHeader> 170 186 <CardTitle>Create a New Annotation</CardTitle> ··· 172 188 Fill out the form below to create an annotation for a URL 173 189 </CardDescription> 174 190 </CardHeader> 175 - 191 + 176 192 <CardContent> 177 193 <form onSubmit={handleSubmit}> 178 194 <div className="space-y-6"> ··· 188 204 required 189 205 /> 190 206 </div> 191 - 207 + 192 208 {/* Template Selection */} 193 209 <div className="space-y-2"> 194 210 <Label htmlFor="template">Annotation Template</Label> ··· 215 231 </Select> 216 232 )} 217 233 </div> 218 - 234 + 219 235 {/* Template Fields */} 220 236 {loading ? ( 221 237 <div className="flex justify-center py-6"> ··· 230 246 <p className="text-sm text-gray-500"> 231 247 {selectedTemplate.description} 232 248 </p> 233 - 249 + 234 250 <div className="space-y-4"> 235 251 {selectedTemplate.fields.map((field) => ( 236 - <div key={field.id} className="p-3 bg-white rounded-md shadow-sm"> 252 + <div 253 + key={field.id} 254 + className="p-3 bg-white rounded-md shadow-sm" 255 + > 237 256 <AnnotationFieldInput 238 257 field={field} 239 258 value={formValues[field.id] || ""} ··· 246 265 ) 247 266 )} 248 267 </div> 249 - 268 + 250 269 <div className="mt-6"> 251 270 <Button 252 271 type="submit"