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 { useShop } from '@/hooks/useShops'; import { LoginArea } from '@/components/auth/LoginArea'; import { SKATE_SHOP_KIND, isValidNoffer } from '@/lib/types'; import { NofferField } from '@/components/NofferField'; import { Store, Upload, X, Loader2, ArrowLeft, MapPin, AlertTriangle, } from 'lucide-react'; import { Link } from 'react-router-dom'; import NotFound from './NotFound'; function EditShopSkeleton() { return (
); } export default function EditShopPage() { const { pubkey, identifier } = useParams<{ pubkey: string; identifier: string }>(); const navigate = useNavigate(); const { user } = useCurrentUser(); const { data: shop, isLoading, error } = useShop(identifier || '', pubkey); const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { toast } = useToast(); // Form state const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [location, setLocation] = useState(''); const [latitude, setLatitude] = useState(''); const [longitude, setLongitude] = useState(''); const [phone, setPhone] = useState(''); const [email, setEmail] = useState(''); const [website, setWebsite] = useState(''); const [instagram, setInstagram] = useState(''); const [hours, setHours] = useState(''); const [noffer, setNoffer] = useState(''); const [images, setImages] = useState([]); const [isGettingLocation, setIsGettingLocation] = useState(false); const [initialized, setInitialized] = useState(false); useSeoMeta({ title: shop ? `Edit ${shop.name} - SkateSpots` : 'Edit Shop - SkateSpots', description: 'Edit your skate shop.', }); // Initialize form with existing data useEffect(() => { if (shop && !initialized) { setName(shop.name); setDescription(shop.description || ''); setLocation(shop.location || ''); setPhone(shop.phone || ''); setEmail(shop.email || ''); setWebsite(shop.website || ''); setInstagram(shop.instagram || ''); setHours(shop.hours || ''); setNoffer(shop.noffer || ''); setImages(shop.images); // Decode geohash to get coordinates if (shop.geohash) { const { latitude: lat, longitude: lng } = ngeohash.decode(shop.geohash); setLatitude(lat.toFixed(6)); setLongitude(lng.toFixed(6)); } setInitialized(true); } }, [shop, initialized]); 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 (!shop) return; if (!name.trim()) { toast({ title: 'Shop name required', description: 'Please enter a name for your shop.', variant: 'destructive', }); return; } if (!location.trim()) { toast({ title: 'Address required', description: 'Please enter your shop address.', 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 tags - use the same identifier to update the addressable event const tags: string[][] = [ ['d', shop.identifier], ['name', name.trim()], ['g', geohash], ['t', 'skateshop'], ['lat', lat.toString()], ['lon', lng.toString()], ['location', location.trim()], ['alt', `Skate shop: ${name.trim()}`], ]; if (phone.trim()) { tags.push(['phone', phone.trim()]); } if (email.trim()) { tags.push(['email', email.trim()]); } if (website.trim()) { tags.push(['website', website.trim()]); } if (instagram.trim()) { tags.push(['instagram', instagram.trim()]); } if (hours.trim()) { tags.push(['hours', hours.trim()]); } if (isValidNoffer(noffer.trim())) { tags.push(['noffer', noffer.trim().toLowerCase()]); } for (const imageUrl of images) { // Use imeta tag for NIP-92 compatibility tags.push(['imeta', `url ${imageUrl}`, 'm image/jpeg']); } await createEvent({ kind: SKATE_SHOP_KIND, content: description.trim(), tags, }); toast({ title: 'Shop updated!', description: 'Your changes have been published to the network.', }); navigate(`/shop/${user?.pubkey}/${shop.identifier}`); } catch (error) { toast({ title: 'Failed to update', description: error instanceof Error ? error.message : 'Something went wrong', variant: 'destructive', }); } }; if (isLoading) { return ; } if (error || !shop) { return ; } // Check if user is the owner if (!user) { return (

Login Required

You need to log in to edit this shop.

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

Access Denied

You can only edit shops that you created.

); } return (
Back to Shop

Edit Shop

Update the details for {shop.name}

Shop Details Update your shop information. All fields marked with * are required. {/* Shop Name */}
setName(e.target.value)} required />
{/* Description */}