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 { Badge } from '@/components/ui/badge';
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 { useSkateEvent } from '@/hooks/useSkateEvents';
import { useEventRsvps, eventCoord } from '@/hooks/useEventRsvps';
import { usePostEventUpdate } from '@/hooks/useEventUpdates';
import { LoginArea } from '@/components/auth/LoginArea';
import { CALENDAR_EVENT_KIND, EVENT_TYPE_LABELS, type EventUpdateKind } from '@/lib/types';
import { cn } from '@/lib/utils';
import {
Calendar,
Upload,
X,
Loader2,
ArrowLeft,
MapPin,
AlertTriangle,
} from 'lucide-react';
import { Link } from 'react-router-dom';
import NotFound from './NotFound';
const EVENT_TYPES = Object.entries(EVENT_TYPE_LABELS);
function formatDateForInput(timestamp: number): string {
const date = new Date(timestamp * 1000);
return date.toISOString().split('T')[0];
}
function formatTimeForInput(timestamp: number): string {
const date = new Date(timestamp * 1000);
return date.toTimeString().slice(0, 5);
}
function EditEventSkeleton() {
return (
<>
>
);
}
export default function EditEventPage() {
const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>();
const navigate = useNavigate();
const { user } = useCurrentUser();
const { data: event, isLoading, error } = useSkateEvent(identifier || '', pubkey);
const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish();
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const { toast } = useToast();
// RSVP audience + host-update poster, so edits can auto-notify attendees.
const rsvpSummary = useEventRsvps(event ? eventCoord(event) : undefined);
const { mutateAsync: postUpdate } = usePostEventUpdate();
// Form state
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [location, setLocation] = useState('');
const [latitude, setLatitude] = useState('');
const [longitude, setLongitude] = useState('');
const [startDate, setStartDate] = useState('');
const [startTime, setStartTime] = useState('');
const [endDate, setEndDate] = useState('');
const [endTime, setEndTime] = useState('');
const [selectedTypes, setSelectedTypes] = useState([]);
const [image, setImage] = useState(null);
const [isGettingLocation, setIsGettingLocation] = useState(false);
const [initialized, setInitialized] = useState(false);
useSeoMeta({
title: event ? `Edit ${event.title} - SkateSpots` : 'Edit Event - SkateSpots',
description: 'Edit your skateboarding event.',
});
// Initialize form with existing data
useEffect(() => {
if (event && !initialized) {
setTitle(event.title);
setDescription(event.description || '');
setLocation(event.location || '');
setSelectedTypes(event.types);
setImage(event.image || null);
// Set dates and times
setStartDate(formatDateForInput(event.start));
setStartTime(formatTimeForInput(event.start));
if (event.end) {
setEndDate(formatDateForInput(event.end));
setEndTime(formatTimeForInput(event.end));
}
// Decode geohash to get coordinates
if (event.geohash) {
const { latitude: lat, longitude: lng } = ngeohash.decode(event.geohash);
setLatitude(lat.toFixed(6));
setLongitude(lng.toFixed(6));
}
setInitialized(true);
}
}, [event, initialized]);
const toggleType = (type: string) => {
setSelectedTypes((prev) =>
prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]
);
};
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) {
setImage(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 = () => {
setImage(null);
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!event) return;
if (!title.trim()) {
toast({
title: 'Title required',
description: 'Please enter a name for this event.',
variant: 'destructive',
});
return;
}
if (!startDate || !startTime) {
toast({
title: 'Start time required',
description: 'Please enter when the event starts.',
variant: 'destructive',
});
return;
}
// Parse start time
const startDateTime = new Date(`${startDate}T${startTime}`);
const startTimestamp = Math.floor(startDateTime.getTime() / 1000);
if (isNaN(startTimestamp)) {
toast({
title: 'Invalid date',
description: 'Please enter a valid start date and time.',
variant: 'destructive',
});
return;
}
// Parse end time if provided
let endTimestamp: number | undefined;
if (endDate && endTime) {
const endDateTime = new Date(`${endDate}T${endTime}`);
endTimestamp = Math.floor(endDateTime.getTime() / 1000);
if (isNaN(endTimestamp) || endTimestamp <= startTimestamp) {
toast({
title: 'Invalid end time',
description: 'End time must be after start time.',
variant: 'destructive',
});
return;
}
}
try {
// Build tags - use the same identifier to update the addressable event
const tags: string[][] = [
['d', event.identifier],
['title', title.trim()],
['start', startTimestamp.toString()],
['t', 'skateboarding'],
['alt', `Skate event: ${title.trim()}`],
];
if (endTimestamp) {
tags.push(['end', endTimestamp.toString()]);
}
if (location.trim()) {
tags.push(['location', location.trim()]);
}
// Add geohash if coordinates provided
if (latitude && longitude) {
const lat = parseFloat(latitude);
const lng = parseFloat(longitude);
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
const geohash = ngeohash.encode(lat, lng, 7);
tags.push(['g', geohash]);
}
}
for (const type of selectedTypes) {
tags.push(['t', type]);
}
if (image) {
tags.push(['image', image]);
}
// Carry through every tag the edit form doesn't manage, untouched. This
// is an addressable event, so re-publishing REPLACES the whole tag set —
// if we only rebuilt the fields on this form we'd silently strip linked
// spots/crews/tours/musicians, the entry fee, the paid "featured" (Pro)
// listing, sponsors, and any other tags an event was created with.
const MANAGED_TAGS = new Set([
'd', 'title', 'start', 'end', 'location', 'g', 'image', 'alt',
]);
// `t` is special: we fully rebuild the topic tags this form owns
// (skateboarding + selected types), but must preserve OTHER `t` values
// like 'paid' that other parts of the app set.
const managedTopics = new Set(['skateboarding', ...selectedTypes]);
for (const tag of event.event.tags) {
const [name, value] = tag;
if (MANAGED_TAGS.has(name)) continue;
if (name === 't') {
if (!managedTopics.has(value)) tags.push(tag); // keep foreign topics (e.g. 'paid')
continue;
}
tags.push(tag); // a-refs, *-name, entry-fee, featured, sponsor, etc.
}
// Detect the changes attendees care about (time / location) BEFORE we
// overwrite the event, so we can auto-notify RSVPers.
const newLocation = location.trim();
const timeChanged = startTimestamp !== event.start || endTimestamp !== event.end;
const locationChanged = newLocation !== (event.location || '');
await createEvent({
kind: CALENDAR_EVENT_KIND,
content: description.trim(),
tags,
});
// Auto-alert attendees when the when/where changed.
const audience = [...rsvpSummary.going, ...rsvpSummary.tentative];
if ((timeChanged || locationChanged) && audience.length > 0) {
const updateKind: EventUpdateKind = timeChanged ? 'reschedule' : 'relocation';
const parts: string[] = [];
if (timeChanged) {
parts.push(
`New time: ${startDateTime.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' })} at ${startDateTime.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}.`,
);
}
if (locationChanged) {
parts.push(newLocation ? `New spot: ${newLocation}.` : 'Location updated.');
}
try {
await postUpdate({
event: { ...event, start: startTimestamp, end: endTimestamp, location: newLocation },
message: `${parts.join(' ')} — see the event for details.`,
updateKind,
notifyPubkeys: audience,
});
} catch {
// Non-fatal: the edit still succeeded even if the alert didn't post.
}
}
const notified = (timeChanged || locationChanged) && audience.length > 0;
toast({
title: 'Event updated!',
description: notified
? `Changes published — ${new Set(audience).size} attendee${new Set(audience).size === 1 ? '' : 's'} notified.`
: 'Your changes have been published to the network.',
});
navigate(`/event/${user?.pubkey}/${event.identifier}`);
} catch (error) {
toast({
title: 'Failed to update',
description: error instanceof Error ? error.message : 'Something went wrong',
variant: 'destructive',
});
}
};
if (isLoading) {
return ;
}
if (error || !event) {
return ;
}
// Check if user is logged in
if (!user) {
return (
<>
Login Required
You need to log in to edit this event.
>
);
}
// Check if user is the owner
if (user.pubkey !== event.pubkey) {
return (
<>
Access Denied
You can only edit events that you created.
>
);
}
return (
<>
Back to Event
Edit Event
Update the details for {event.title}
>
);
}