OWHNUYOKSGQTG6ZSVQZ3DNMYCW3IOEMXGRRS5QRRDAPIG4MOODNAC E7L6ECZK5DLB27CDJSB3USBUKU4E2HE4GDNUIQJB6HNU4J65MRMAC E3WSKRJTPPRD6ZEEURTO77N6GTHYJY7ISWQL2LN2VCCXA4YRH2CAC UCDTBEK3CF6YT2H6V57HI6FAFW44BIYYAK3Z2QJ5LJE7QWX7OEYAC JHFIJJSLVMQNYIDE6CXWUK5UTB7BTUSY7NCI33WKCWIRZHCSMBHQC YX7LU4WRAUDMWS3DEDXZDSF6DXBHLYDWVSMSRK6KIW3MO6GRXSVQC RLH37YB4D7O42IFM2T7GJG4AVVAURWBZ7AOTHAWR7YJZRG3JOPLQC 4M3EBLTLSS2BRCM42ZP7WVD4YMRRLGV2P2XF47IAV5XHHJD52HTQC M4UG5FMI5ICQRLJCCNSLV3ZKCGIXDQ65ECJM4PBE3NZYHT3CJLDAC CVW5G63BAFGQBR5YTZIHTZVVVECP2U3XI76APTKKOODGWHRBEIBQC POIBWSL3JFHT2KN3STFSJX3INSYKEJTX6KSW3N7BVEKWX2GJ6T7QC O7W4FZVRKDQDAAXEW4T7P262PPRILRCSSACODMUTQZ6VNR36PVCQC DOQBQX4IQSDBYYSBP4BEMTMJUKZPSC33KKXPAGZ3A5BRJMMKHCRQC 2OYSY7VNMQ4C3CQMX7VKKD2JSZM72TJ3LL4GEDUQYG6RJTNIROLAC M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC details: error instanceof Error ? error.message : String(error),},500);}});/*** Protected API route to create a new location** @route POST /api/locations* @authentication Required* @param {Object} body - Location data including datasetId, name, description, latitude, longitude* @returns {Object} Response containing:* - data: Created location object* @description Creates a new location with the authenticated user as owner.* Requires EDIT permission on the dataset to create locations within it.*/app.post("/api/locations", authenticate, async (c) => {try {// Get user ID from JWTconst jwtPayload = (c as unknown as { jwtPayload: JWTPayload }).jwtPayload;const userId = jwtPayload.sub;// Database connectionconst sql = neon(c.env.DATABASE_URL);const db = drizzle(sql);// Parse and validate request bodyconst body = await c.req.json();const { id, datasetId, name, description, latitude, longitude } = body;// Field validationif (!id || typeof id !== 'string') {return c.json({error: "Missing or invalid required field: id"}, 400);}if (!datasetId || typeof datasetId !== 'string') {return c.json({error: "Missing or invalid required field: datasetId"}, 400);}if (!name || typeof name !== 'string' || name.trim().length === 0) {return c.json({error: "Missing or invalid required field: name"}, 400);}// Length validationif (id.length !== 12) {return c.json({error: "Field 'id' must be exactly 12 characters (nanoid)"}, 400);}if (name.length > 140) {return c.json({error: "Field 'name' must be 140 characters or less"}, 400);}if (description && description.length > 255) {return c.json({error: "Field 'description' must be 255 characters or less"}, 400);}// Validate latitude and longitude if providedif (latitude !== null && latitude !== undefined) {const lat = Number(latitude);if (isNaN(lat) || lat < -90 || lat > 90) {return c.json({error: "Field 'latitude' must be a valid number between -90 and 90"}, 400);}}if (longitude !== null && longitude !== undefined) {const lng = Number(longitude);if (isNaN(lng) || lng < -180 || lng > 180) {return c.json({error: "Field 'longitude' must be a valid number between -180 and 180"}, 400);}}// Check if user has EDIT permission on the datasetconst hasPermission = await checkUserPermission(db, userId, datasetId.trim(), 'EDIT');if (!hasPermission) {return c.json({error: "You don't have permission to create locations in this dataset"}, 403);}// Create location objectconst now = new Date();const newLocation = {id: id.trim(),datasetId: datasetId.trim(),name: name.trim(),description: description?.trim() || null,latitude: latitude !== null && latitude !== undefined ? String(Number(latitude)) : null,longitude: longitude !== null && longitude !== undefined ? String(Number(longitude)) : null,createdBy: userId,createdAt: now,lastModified: now,modifiedBy: userId,active: true,};// Insert the locationconst result = await db.insert(location).values(newLocation).returning({id: location.id,datasetId: location.datasetId,name: location.name,description: location.description,latitude: location.latitude,longitude: location.longitude,createdAt: location.createdAt,createdBy: location.createdBy,lastModified: location.lastModified,modifiedBy: location.modifiedBy,active: location.active,});console.log("Created location:", result[0].id, "for dataset:", datasetId, "by user:", userId);return c.json({data: result[0]}, 201);} catch (error) {console.error("Error creating location:", error);// Handle unique constraint violationsif (error instanceof Error && error.message.includes('duplicate key')) {return c.json({error: "A location with this ID already exists"}, 400);}return c.json({error: "Failed to create location",
const handleSubmitLocation = async (e: React.FormEvent) => {e.preventDefault();if (!isAuthenticated) return;setFormLoading(true);setFormError(null);try {const accessToken = await getAccessToken();const id = nanoid(12);const response = await fetch("/api/locations", {method: "POST",headers: {"Authorization": `Bearer ${accessToken}`,"Content-Type": "application/json",},body: JSON.stringify({id,datasetId,name: formData.name,description: formData.description || null,latitude: formData.latitude ? parseFloat(formData.latitude) : null,longitude: formData.longitude ? parseFloat(formData.longitude) : null,}),});if (!response.ok) {const errorData = await response.json();throw new Error(errorData.error || `HTTP error! Status: ${response.status}`);}const { data: newLocation } = await response.json();// Add the new location to the listsetLocations(prev => [newLocation, ...prev]);// Reset form and close modalhandleCancelForm();} catch (err) {setFormError(err instanceof Error ? err.message : "Failed to create location");console.error("Error creating location:", err);} finally {setFormLoading(false);}};
const handleFormChange = (field: keyof typeof formData, value: string) => {setFormData(prev => ({...prev,[field]: value}));};const handleCancelForm = () => {setShowForm(false);setFormError(null);setFormData({name: '',description: '',latitude: '',longitude: ''});};const handleCreateNew = () => {setFormData({name: '',description: '',latitude: '',longitude: ''});setShowForm(true);};
<div className="card p-6 bg-white shadow-sm rounded-lg">
<>{/* Header with Add Button */}{isAuthenticated && !authLoading && canCreateLocations && (<div className="flex justify-between items-center mb-4"><h2 className="text-lg font-semibold">Locations</h2><ButtononClick={handleCreateNew}variant="default"size="sm"className="flex items-center gap-2"><Plus className="h-4 w-4" />Add Location</Button></div>)}<div className="card p-6 bg-white shadow-sm rounded-lg">
</div>
</div>{/* Create Location Modal */}{showForm && (<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"><div className="bg-white rounded-lg p-6 w-full max-w-md mx-4"><div className="flex justify-between items-center mb-4"><h3 className="text-lg font-semibold">Create New Location</h3><ButtononClick={handleCancelForm}variant="ghost"size="sm"className="p-1"><X className="h-4 w-4" /></Button></div>{formError && (<div className="bg-red-50 border border-red-200 text-red-700 px-3 py-2 rounded mb-4">{formError}</div>)}<form onSubmit={handleSubmitLocation} className="space-y-4"><div><label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">Name *</label><inputtype="text"id="name"requiredmaxLength={255}value={formData.name}onChange={(e) => handleFormChange('name', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="Enter location name"/></div><div><label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">Description</label><textareaid="description"maxLength={255}value={formData.description}onChange={(e) => handleFormChange('description', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="Enter description (optional)"rows={3}/></div><div className="grid grid-cols-2 gap-4"><div><label htmlFor="latitude" className="block text-sm font-medium text-gray-700 mb-1">Latitude</label><inputtype="number"id="latitude"step="any"min="-90"max="90"value={formData.latitude}onChange={(e) => handleFormChange('latitude', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="e.g. -45.50608"/></div><div><label htmlFor="longitude" className="block text-sm font-medium text-gray-700 mb-1">Longitude</label><inputtype="number"id="longitude"step="any"min="-180"max="180"value={formData.longitude}onChange={(e) => handleFormChange('longitude', e.target.value)}className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"placeholder="e.g. 167.47822"/></div></div><div className="flex justify-end space-x-3 pt-4"><Buttontype="button"onClick={handleCancelForm}variant="outline"disabled={formLoading}>Cancel</Button><Buttontype="submit"disabled={formLoading || !formData.name.trim()}>{formLoading ? "Creating..." : "Create Location"}</Button></div></form></div></div>)}</>