import { useEffect, useState } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useNavigate, Link } from 'react-router-dom'; import ngeohash from 'ngeohash'; import { Layout } from '@/components/Layout'; import { Card, CardContent } 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 { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useToast } from '@/hooks/useToast'; import { LoginArea } from '@/components/auth/LoginArea'; import { useUserSkaterProfile, SKATER_D } from '@/hooks/useSkaters'; import { useSkateSpots } from '@/hooks/useSkateSpots'; import { SKATE_SKATER_KIND, SKATE_SPOT_KIND, SKATER_TAG, SKATER_STANCES, SKATER_STYLES, SKATER_LEVELS, type SkaterStance, type SkaterStyle, type SkaterLevel, type SkateSpot, } from '@/lib/types'; import { genUserName } from '@/lib/genUserName'; import { cn } from '@/lib/utils'; import { X, Loader2, ArrowLeft, MapPin, PersonStanding, Sparkles, UserRound, ShieldCheck, Zap, FileText, Globe, Map as MapIcon, Search, Check } from 'lucide-react'; export default function BecomeSkaterPage() { useSeoMeta({ title: 'Join the Skater Directory - SkateSpots', description: 'Create your skater profile so the community can find you.', }); const navigate = useNavigate(); const { user } = useCurrentUser(); const author = useAuthor(user?.pubkey || ''); const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish(); const { toast } = useToast(); const { data: existing, isLoading: loadingExisting } = useUserSkaterProfile(user?.pubkey); const { data: allSpots, isLoading: loadingSpots } = useSkateSpots(500); const metadata = author.data?.metadata; const displayName = metadata?.name ?? (user ? genUserName(user.pubkey) : 'Skater'); const avatar = metadata?.picture; // kind-0 values we borrow from so we never re-ask for what the profile has. const kind0About = metadata?.about ?? ''; const kind0Website = metadata?.website ?? ''; const kind0Nip05 = metadata?.nip05 ?? ''; const kind0Lud16 = metadata?.lud16 ?? ''; const [bio, setBio] = useState(''); const [location, setLocation] = useState(''); const [latitude, setLatitude] = useState(''); const [longitude, setLongitude] = useState(''); const [stance, setStance] = useState(); const [styles, setStyles] = useState([]); const [level, setLevel] = useState(); // When set, the skater is using a free-text level instead of a preset. const [customLevel, setCustomLevel] = useState(''); const [levelMode, setLevelMode] = useState<'preset' | 'custom'>('preset'); const [years, setYears] = useState(''); const [homeSpot, setHomeSpot] = useState(''); // When the home spot is a real mapped spot, we keep its `a` coordinate here. const [homeSpotRef, setHomeSpotRef] = useState(); const [spotPickerOpen, setSpotPickerOpen] = useState(false); const [spotSearch, setSpotSearch] = useState(''); const [instagram, setInstagram] = useState(''); const [website, setWebsite] = useState(''); const [isGettingLocation, setIsGettingLocation] = useState(false); // Ensures we only auto-prefill from kind-0 once, and never clobber typing. const [prefilled, setPrefilled] = useState(false); // Prefill: existing 35000 wins; otherwise borrow shared fields from kind-0. useEffect(() => { if (prefilled) return; if (existing) { setBio(existing.bio || ''); setLocation(existing.location || ''); setStance(existing.stance); setStyles(existing.styles || []); setLevel(existing.level); if (existing.customLevel && !existing.level) { setCustomLevel(existing.customLevel); setLevelMode('custom'); } setYears(existing.yearsSkating || ''); setHomeSpot(existing.homeSpot || ''); setHomeSpotRef(existing.homeSpotRef); setInstagram(existing.instagram || ''); setWebsite(existing.website || kind0Website); setPrefilled(true); } else if (author.data) { // New skater — seed the shared fields from their Nostr profile. setBio(kind0About); setWebsite(kind0Website); setPrefilled(true); } }, [existing, author.data, kind0About, kind0Website, prefilled]); const toggleStyle = (s: SkaterStyle) => setStyles((prev) => (prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s])); const getCurrentLocation = () => { if (!navigator.geolocation) { toast({ title: 'Geolocation not supported', variant: 'destructive' }); return; } setIsGettingLocation(true); navigator.geolocation.getCurrentPosition( (pos) => { setLatitude(pos.coords.latitude.toFixed(6)); setLongitude(pos.coords.longitude.toFixed(6)); setIsGettingLocation(false); toast({ title: 'Location found' }); }, (err) => { setIsGettingLocation(false); toast({ title: 'Location error', description: err.message, variant: 'destructive' }); }, ); }; // Pick a real mapped spot as the home spot — stores its title + `a` coordinate. const selectSpot = (spot: SkateSpot) => { setHomeSpot(spot.title); setHomeSpotRef(`${SKATE_SPOT_KIND}:${spot.pubkey}:${spot.identifier}`); setSpotPickerOpen(false); setSpotSearch(''); }; const clearSpotLink = () => setHomeSpotRef(undefined); const filteredSpots = (allSpots ?? []).filter((s) => { if (!spotSearch.trim()) return true; const q = spotSearch.toLowerCase(); return ( s.title.toLowerCase().includes(q) || (s.location?.toLowerCase().includes(q) ?? false) ); }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!user) return; if (styles.length === 0) { toast({ title: 'Pick your styles', description: 'Choose at least one thing you skate.', variant: 'destructive' }); return; } try { const tags: string[][] = [ ['d', SKATER_D], ['title', displayName], ['t', SKATER_TAG], ['t', 'skateboarding'], ['alt', `SkateSpots skater: ${displayName}`], ]; if (location.trim()) tags.push(['location', location.trim()]); if (latitude && longitude) { const lat = parseFloat(latitude); const lng = parseFloat(longitude); if (!isNaN(lat) && !isNaN(lng)) tags.push(['g', ngeohash.encode(lat, lng, 7)]); } if (stance) tags.push(['stance', stance]); if (levelMode === 'custom' && customLevel.trim()) { tags.push(['level_custom', customLevel.trim().slice(0, 40)]); } else if (levelMode === 'preset' && level) { tags.push(['level', level]); } for (const s of styles) tags.push(['t', s]); if (years.trim()) tags.push(['years', years.trim()]); if (homeSpot.trim()) tags.push(['home_spot', homeSpot.trim()]); // Link to the real mapped spot when one was picked from the map. if (homeSpotRef) tags.push(['a', homeSpotRef]); if (instagram.trim()) tags.push(['instagram', instagram.trim()]); // Store shared fields ONLY as overrides — if they match kind-0, leave them // out so kind-0 stays the single source of truth and nothing goes stale. // Photo is intentionally NOT stored here: the skater card always uses the // kind-0 profile picture so the two stay identical (single source of truth). const websiteTrim = website.trim(); if (websiteTrim && websiteTrim !== kind0Website.trim()) { tags.push(['website', websiteTrim]); } const bioTrim = bio.trim(); const content = bioTrim === kind0About.trim() ? '' : bioTrim; await createEvent({ kind: SKATE_SKATER_KIND, content, tags }); toast({ title: existing ? 'Profile updated!' : "You're on the map! 🛹", description: 'Other skaters can now discover you.', }); navigate('/skaters'); } catch (error) { toast({ title: 'Failed to publish', description: error instanceof Error ? error.message : 'Error', variant: 'destructive' }); } }; if (!user) { return (

Log in to join

Create your skater profile so the community can find you.

); } const styleEntries = Object.entries(SKATER_STYLES) as [SkaterStyle, { label: string; emoji: string }][]; const stanceEntries = Object.entries(SKATER_STANCES) as [SkaterStance, { label: string; emoji: string }][]; const levelEntries = Object.entries(SKATER_LEVELS) as [SkaterLevel, { label: string; emoji: string; color: string }][]; return (
Back to Skaters {/* Hero */}
{displayName.slice(0, 2).toUpperCase()}

{existing ? 'Edit your' : 'Join the'} Skater Directory

Publishing as {displayName}

{/* Linked-to-your-profile notice + read-only summary of borrowed fields */}

This is your skate layer, built on your Nostr profile. Your name, photo, bio, lightning & verification come from there — edit your full profile to change them everywhere.

{/* Chips showing EVERYTHING carried over from your kind-0 profile */}
{avatar && ( Photo )} {displayName} {kind0About.trim() && ( Bio )} {kind0Website.trim() && ( Website )} {kind0Nip05 && ( {kind0Nip05} )} {kind0Lud16 && ( Zaps on )}
{loadingExisting ? (
Loading your profile…
) : (
{/* Bio */}
{bio.trim() && bio.trim() === kind0About.trim() && ( From your profile )}