import { useState, useEffect } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useNavigate, useSearchParams } 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 { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useToast } from '@/hooks/useToast'; import { ShopBountyCard } from '@/components/ShopBountyCard'; import { LoginArea } from '@/components/auth/LoginArea'; import { SKATE_SHOP_KIND, isValidNoffer } from '@/lib/types'; import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions'; import { NofferField } from '@/components/NofferField'; import { Store, Upload, X, Loader2, ArrowLeft, MapPin, } from 'lucide-react'; import { Link } from 'react-router-dom'; export default function CreateShopPage() { useSeoMeta({ title: 'Add a Skate Shop - SkateSpots', description: 'Add your skate shop to the community.', }); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { user } = useCurrentUser(); 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 [noffer, setNoffer] = useState(''); const [hours, setHours] = useState(''); const [images, setImages] = useState([]); const [isGettingLocation, setIsGettingLocation] = useState(false); // Pre-fill coordinates from URL params (from map click) useEffect(() => { const lat = searchParams.get('lat'); const lng = searchParams.get('lng'); if (lat && lng) { setLatitude(lat); setLongitude(lng); } }, [searchParams]); 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', }); } // Clear the input e.target.value = ''; }; const removeImage = (url: string) => { setImages((prev) => prev.filter((img) => img !== url)); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); 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); // Create unique identifier for addressable event const identifier = name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .slice(0, 50) + '-' + Date.now().toString(36); // Best-effort reverse geocode → city/country/country_code, so shops show // up in the /stats region breakdown (same helper as spots; never throws). const geo = await reverseGeocodeRegions(lat, lng); // Build tags according to NIP-15 and custom extensions const tags: string[][] = [ ['d', identifier], // Required for addressable events ['name', name.trim()], ['g', geohash], ['t', 'skateshop'], // Primary identifier for shop events ['lat', lat.toString()], ['lon', lng.toString()], ['location', location.trim()], ]; pushRegionTags(tags, geo); 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 added!', description: 'Your skate shop is now on the map. Skaters can zap you to say thanks!', }); // Navigate to shop detail using pubkey and identifier navigate(`/shop/${user?.pubkey}/${identifier}`); } catch (error) { console.error('Failed to create shop:', error); toast({ title: 'Failed to publish', description: error instanceof Error ? error.message : 'Something went wrong. Check console for details.', variant: 'destructive', }); } }; if (!user) { return ( <>

Login Required

You need to log in to add a skate shop.

); } return ( <>
Back to Map

Add a Skate Shop

List your shop on the map and connect with the community

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