import { useState, useEffect } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useNavigate, useParams } from 'react-router-dom'; import ngeohash from 'ngeohash'; import { Layout } from '@/components/Layout'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Skeleton } from '@/components/ui/skeleton'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useToast } from '@/hooks/useToast'; import { useSkateSpot } from '@/hooks/useSkateSpots'; import { LoginArea } from '@/components/auth/LoginArea'; import { TagInput } from '@/components/TagInput'; import { SKATE_SPOT_KIND } from '@/lib/types'; import { spotCategories, MAX_TAGS } from '@/lib/skateTags'; import { cn } from '@/lib/utils'; import { MapPin, Upload, X, Loader2, ArrowLeft, AlertTriangle, Check, } from 'lucide-react'; import { Link } from 'react-router-dom'; import NotFound from './NotFound'; function EditSpotSkeleton() { return (
); } export default function EditSpotPage() { const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const navigate = useNavigate(); const { user } = useCurrentUser(); const { data: spot, isLoading, error } = useSkateSpot(identifier || '', pubkey); const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { toast } = useToast(); // Form state const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [location, setLocation] = useState(''); const [latitude, setLatitude] = useState(''); const [longitude, setLongitude] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); const [tags, setTags] = useState([]); const [accessType, setAccessType] = useState<'public' | 'private' | 'locals'>('public'); const [contactInfo, setContactInfo] = useState(''); const [images, setImages] = useState([]); const [isGettingLocation, setIsGettingLocation] = useState(false); const [initialized, setInitialized] = useState(false); useSeoMeta({ title: spot ? `Edit ${spot.title} - SkateSpots` : 'Edit Spot - SkateSpots', description: 'Edit your skateboarding spot.', }); // Initialize form with existing data useEffect(() => { if (spot && !initialized) { setTitle(spot.title); setDescription(spot.description || ''); setLocation(spot.location || ''); setAccessType(spot.accessType || 'public'); setContactInfo(spot.contactInfo || ''); setImages(spot.images); // Set category (new format) or extract from types (legacy) setSelectedCategory(spot.category || ''); // Set tags (new format) setTags(spot.tags || []); // Decode geohash to get coordinates if (spot.geohash) { const { latitude: lat, longitude: lng } = ngeohash.decode(spot.geohash); setLatitude(lat.toFixed(6)); setLongitude(lng.toFixed(6)); } setInitialized(true); } }, [spot, initialized]); const handleCategoryChange = (category: string) => { setSelectedCategory(category); }; const getCurrentLocation = () => { if (!navigator.geolocation) { toast({ title: 'Geolocation not supported', description: 'Your browser does not support geolocation.', variant: 'destructive', }); return; } setIsGettingLocation(true); navigator.geolocation.getCurrentPosition( (position) => { setLatitude(position.coords.latitude.toFixed(6)); setLongitude(position.coords.longitude.toFixed(6)); setIsGettingLocation(false); toast({ title: 'Location found', description: 'Your current location has been set.', }); }, (error) => { setIsGettingLocation(false); toast({ title: 'Location error', description: error.message, variant: 'destructive', }); } ); }; const handleImageUpload = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; try { const file = files[0]; const tags = await uploadFile(file); const url = tags[0]?.[1]; if (url) { setImages((prev) => [...prev, url]); toast({ title: 'Image uploaded', description: 'Your image has been uploaded successfully.', }); } } catch (error) { toast({ title: 'Upload failed', description: error instanceof Error ? error.message : 'Failed to upload image', variant: 'destructive', }); } e.target.value = ''; }; const removeImage = (url: string) => { setImages((prev) => prev.filter((img) => img !== url)); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!spot) return; if (!title.trim()) { toast({ title: 'Title required', description: 'Please enter a name for this spot.', variant: 'destructive', }); return; } if (!latitude || !longitude) { toast({ title: 'Location required', description: 'Please enter coordinates or use your current location.', variant: 'destructive', }); return; } const lat = parseFloat(latitude); const lng = parseFloat(longitude); if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) { toast({ title: 'Invalid coordinates', description: 'Please enter valid latitude (-90 to 90) and longitude (-180 to 180).', variant: 'destructive', }); return; } try { // Generate geohash from coordinates (7 chars = ~153m precision) const geohash = ngeohash.encode(lat, lng, 7); // Build event tags - use the same identifier to update the addressable event const eventTags: string[][] = [ ['d', spot.identifier], ['title', title.trim()], ['g', geohash], ['t', 'skatespot'], ['access', accessType], ['lat', lat.toString()], ['lon', lng.toString()], ['alt', `Skate spot: ${title.trim()}`], ]; if (location.trim()) { eventTags.push(['location', location.trim()]); } if (contactInfo.trim()) { eventTags.push(['contact', contactInfo.trim()]); } // Add category as dedicated tag (new format) if (selectedCategory) { eventTags.push(['category', selectedCategory]); // Also add as t tag for backwards compatibility and relay filtering eventTags.push(['t', selectedCategory]); } // Add all freeform tags for (const tag of tags) { eventTags.push(['t', tag]); } for (const imageUrl of images) { // Use imeta tag for NIP-92 compatibility eventTags.push(['imeta', `url ${imageUrl}`, 'm image/jpeg']); } await createEvent({ kind: SKATE_SPOT_KIND, content: description.trim(), tags: eventTags, }); toast({ title: 'Spot updated!', description: 'Your changes have been published to the network.', }); navigate(`/spot/${user?.pubkey}/${spot.identifier}`); } catch (error) { toast({ title: 'Failed to update', description: error instanceof Error ? error.message : 'Something went wrong', variant: 'destructive', }); } }; if (isLoading) { return ; } if (error || !spot) { return ; } // Check if user is the owner if (!user) { return (

Login Required

You need to log in to edit this spot.

); } if (user.pubkey !== spot.pubkey) { return (

Access Denied

You can only edit spots that you created.

); } return (
Back to Spot

Edit Spot

Update the details for {spot.title}

Spot Details Update the information about this spot. All fields marked with * are required. {/* Title */}
setTitle(e.target.value)} required />
{/* Description */}