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 { 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]); } // Preserve sponsors from the original event (the edit form doesn't manage // them yet, so carry them through untouched instead of dropping them). for (const sponsorTag of event.event.tags.filter(([name]) => name === 'sponsor')) { tags.push(sponsorTag); } // 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. Back to Event ); } return ( Back to Event Edit Event Update the details for {event.title} Event Details Update the information about this event. All fields marked with * are required. {/* Title */} Event Name * setTitle(e.target.value)} required /> {/* Description */} Description setDescription(e.target.value)} rows={4} /> {/* Start Date/Time */} Start Date & Time * setStartDate(e.target.value)} required /> setStartTime(e.target.value)} required /> {/* End Date/Time */} End Date & Time (optional) setEndDate(e.target.value)} /> setEndTime(e.target.value)} /> {/* Location Address */} Venue / Address setLocation(e.target.value)} /> {/* Coordinates (optional for events) */} Coordinates (optional) setLatitude(e.target.value)} /> setLongitude(e.target.value)} /> {isGettingLocation ? ( <> Getting location... > ) : ( <> Use my current location > )} {/* Event Types */} Event Type(s) {EVENT_TYPES.map(([type, label]) => ( toggleType(type)} > {label} ))} {/* Cover Image */} Cover Image {image ? ( ) : ( {isUploading ? ( ) : ( <> Upload cover image > )} )} {/* Submit */} {isPublishing ? ( <> Saving... > ) : ( <> Save Changes > )} ); }
You need to log in to edit this event.
You can only edit events that you created.
Update the details for {event.title}