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 { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor'; 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'; import { useTranslation } from '@/contexts/language'; export default function CreateShopPage() { const { t } = useTranslation(); 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 [nip05s, setNip05s] = 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: t('placesUi.createEvent.geoNotSupported'), description: t('placesUi.createEvent.geoNotSupportedDesc'), 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('placesUi.createEvent.locationFound'), description: t('placesUi.createEvent.locationFoundDesc'), }); }, (error) => { setIsGettingLocation(false); toast({ title: t('placesUi.createEvent.locationError'), 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: t('placesUi.createEvent.imageUploaded'), description: t('placesUi.createEvent.imageUploadedDesc'), }); } } catch (error) { toast({ title: t('placesUi.createEvent.uploadFailed'), description: error instanceof Error ? error.message : t('placesUi.createEvent.uploadFailedDesc'), 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: t('placesUi.createShop.nameRequired'), description: t('placesUi.createShop.nameRequiredDesc'), variant: 'destructive', }); return; } if (!location.trim()) { toast({ title: t('placesUi.createShop.addressRequired'), description: t('placesUi.createShop.addressRequiredDesc'), variant: 'destructive', }); return; } if (!latitude || !longitude) { toast({ title: t('placesUi.createShop.locationRequired'), description: t('placesUi.createShop.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('placesUi.createShop.invalidCoords'), description: t('placesUi.createShop.invalidCoordsDesc'), 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 handle of nip05s) tags.push(['nip05', handle]); 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: t('placesUi.createShop.created'), description: t('placesUi.createShop.createdDesc'), }); // Navigate to shop detail using pubkey and identifier navigate(`/shop/${user?.pubkey}/${identifier}`); } catch (error) { console.error('Failed to create shop:', error); toast({ title: t('placesUi.createEvent.publishFailed'), description: error instanceof Error ? error.message : t('placesUi.createShop.publishFailedDesc'), variant: 'destructive', }); } }; if (!user) { return ( <>

{t('errors.loginRequired')}

{t('placesUi.createShop.loginToAdd')}

); } return ( <>
{t('placesUi.shop.backToMap')}

{t('shops.addShop')}

{t('placesUi.createShop.subtitle')}

{t('shops.shopDetails')} {t('placesUi.createShop.detailsDesc')} {/* Shop Name */}
setName(e.target.value)} required />
{/* Description */}