import { useState, useEffect } from 'react';
import { useSeoMeta } from '@unhead/react';
import { useNavigate, useParams } from 'react-router-dom';
import ngeohash from 'ngeohash';
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 { useTranslation } from '@/contexts/language';
import { LoginArea } from '@/components/auth/LoginArea';
import { PhotoDropzone } from '@/components/PhotoDropzone';
import { TagInput } from '@/components/TagInput';
import { SKATE_SPOT_KIND } from '@/lib/types';
import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions';
import { spotCategories, MAX_TAGS, shelterOptions, DEFAULT_SHELTER, SHELTERED_VALUES, type ShelterType } from '@/lib/skateTags';
import { SPOT_TAG_CATEGORY_KEYS, SPOT_TAG_CATEGORY_DESC_KEYS, SHELTER_KEYS, SHELTER_DESC_KEYS } from '@/lib/labelKeys';
import { cn } from '@/lib/utils';
import {
MapPin,
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 { t } = useTranslation();
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 [shelter, setShelter] = useState(DEFAULT_SHELTER);
const [contactInfo, setContactInfo] = useState('');
const [images, setImages] = useState([]);
const [isGettingLocation, setIsGettingLocation] = useState(false);
const [initialized, setInitialized] = useState(false);
useSeoMeta({
title: spot ? `${t('spots.editSpot')} ${spot.title} - SkateSpots` : `${t('spots.editSpot')} - SkateSpots`,
description: t('spotUi.editSpotSeoDesc'),
});
// Initialize form with existing data
useEffect(() => {
if (spot && !initialized) {
setTitle(spot.title);
setDescription(spot.description || '');
setLocation(spot.location || '');
setAccessType(spot.accessType || 'public');
setShelter(spot.shelter || DEFAULT_SHELTER);
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: t('spotUi.geolocationNotSupported'),
description: t('spotUi.geolocationNotSupportedDesc'),
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: t('spotUi.locationFound'),
description: t('spotUi.locationFoundDesc'),
});
},
(error) => {
setIsGettingLocation(false);
toast({
title: t('spotUi.locationError'),
description: error.message,
variant: 'destructive',
});
}
);
};
// Upload one or more selected/dropped photos, in sequence.
const uploadImageFiles = async (files: File[]) => {
if (files.length === 0) return;
let added = 0;
try {
for (const file of files) {
const tags = await uploadFile(file);
const url = tags[0]?.[1];
if (url) {
setImages((prev) => (prev.includes(url) ? prev : [...prev, url]));
added += 1;
}
}
if (added > 0) {
toast({
title: added > 1 ? t('spotUi.photosUploaded', { count: added }) : t('spotUi.imageUploaded'),
description:
added > 1 ? t('spotUi.photosAdded') : t('spotUi.imageUploadedDesc'),
});
}
} catch (error) {
toast({
title: t('spotUi.uploadFailed'),
description: error instanceof Error ? error.message : t('spotUi.failedToUploadImage'),
variant: 'destructive',
});
}
};
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: t('spotUi.titleRequired'),
description: t('spotUi.titleRequiredDesc'),
variant: 'destructive',
});
return;
}
if (!latitude || !longitude) {
toast({
title: t('spotUi.locationRequired'),
description: t('spotUi.locationRequiredDesc'),
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: t('spotUi.invalidCoordinates'),
description: t('spotUi.invalidCoordinatesDesc'),
variant: 'destructive',
});
return;
}
if (!selectedCategory) {
toast({
title: t('spotUi.spotTypeRequired'),
description: t('spotUi.spotTypeRequiredDesc'),
variant: 'destructive',
});
return;
}
try {
// Generate geohash from coordinates (7 chars = ~153m precision)
const geohash = ngeohash.encode(lat, lng, 7);
// Best-effort reverse geocode → city/country/country_code. Re-run on edit
// so moving a spot updates its region, AND so a legacy spot that has no
// tags gets them the next time it's saved. (This page rebuilds tags from
// scratch, so without this it silently DROPPED any existing region tags.)
const geo = await reverseGeocodeRegions(lat, lng);
// 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', t('spotUi.altSkateSpot', { title: title.trim() })],
];
pushRegionTags(eventTags, geo);
if (location.trim()) {
eventTags.push(['location', location.trim()]);
}
if (contactInfo.trim()) {
eventTags.push(['contact', contactInfo.trim()]);
}
// Shelter / weather-protection level (see CreateSpotPage for rationale).
eventTags.push(['shelter', shelter]);
if (SHELTERED_VALUES.includes(shelter)) {
eventTags.push(['t', shelter]);
}
// 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: t('spotUi.spotUpdated'),
description: t('spotUi.spotUpdatedDesc'),
});
navigate(`/spot/${user?.pubkey}/${spot.identifier}`);
} catch (error) {
toast({
title: t('spotUi.failedToUpdate'),
description: error instanceof Error ? error.message : t('spotUi.somethingWentWrong'),
variant: 'destructive',
});
}
};
if (isLoading) {
return ;
}
if (error || !spot) {
return ;
}
// Check if user is the owner
if (!user) {
return (
<>
{t('spotUi.loginRequiredTitle')}
{t('spotUi.loginToEditSpot')}
>
);
}
if (user.pubkey !== spot.pubkey) {
return (
<>
{t('spotUi.accessDeniedTitle')}
{t('spotUi.accessDeniedDesc')}
>
);
}
return (
<>
{t('spotUi.backToSpot')}
{t('spots.editSpot')}
{t('spotUi.updateDetailsFor', { title: spot.title })}
>
);
}