import { useState, useEffect } from 'react'; import { useSeoMeta } from '@unhead/react'; import { useNavigate, Link } from 'react-router-dom'; import ngeohash from 'ngeohash'; import { nip19 } from 'nostr-tools'; 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 { useAuthor } from '@/hooks/useAuthor'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useToast } from '@/hooks/useToast'; import { useWallet } from '@/hooks/useWallet'; import { WalletModal } from '@/components/WalletModal'; import { LoginArea } from '@/components/auth/LoginArea'; import { Nip05Required } from '@/components/Nip05Required'; import { SpotPicker } from '@/components/SpotPicker'; import { PhotoDropzone } from '@/components/PhotoDropzone'; import { ImageWithFallback } from '@/components/ImageWithFallback'; import { NofferField } from '@/components/NofferField'; import { PaymentReadinessNote } from '@/components/PaymentReadinessNote'; import { EntityTypeField } from '@/components/EntityTypeField'; import { CrewLinkField } from '@/components/CrewLinkField'; import { SKATE_COACH_KIND, SKATE_SPOT_KIND, COACH_SKILL_LABELS, COACH_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType } from '@/lib/types'; import { COACH_SKILL_KEYS, COACH_SKILL_DESC_KEYS } from '@/lib/labelKeys'; import { COACH_FORMATS, COACH_LEVELS, COACH_AGE_GROUPS, COACH_CREDENTIALS, COACH_AVAILABILITY, COACH_RATE_CURRENCIES, COACH_RATE_UNITS, COACH_PAYMENT_METHODS, type CoachOption } from '@/lib/coachOptions'; import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl'; import { useTranslation } from '@/contexts/language'; import { cn } from '@/lib/utils'; import { GraduationCap, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Check, Eye } from 'lucide-react'; import { LabProUpgrade } from '@/components/LabProUpgrade'; import { LabProfilePreview } from '@/components/LabProfilePreview'; import { LabDraftBanner } from '@/components/LabDraftBanner'; import { LabProfileProgress } from '@/components/LabProfileProgress'; import { useLabDraft } from '@/hooks/useLabDraft'; import { labProfilePath } from '@/lib/labRoutes'; import { FormWizard, WizardStep } from '@/components/FormWizard'; import { usePlatformFee } from '@/hooks/usePlatformFee'; import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile'; const SKILL_ENTRIES = Object.entries(COACH_SKILL_LABELS); /** Section divider/heading inside the (long) coach form. */ function FormSection({ title, hint }: { title: string; hint?: string }) { return (

{title}

{hint &&

{hint}

}
); } /** Reusable chip toggle row for the curated coach option sets. */ function OptionChips({ label, hint, options, selected, onToggle, idPrefix, }: { label: string; hint?: string; options: CoachOption[]; selected: string[]; onToggle: (value: string) => void; idPrefix: string; }) { const { t } = useTranslation(); return (
{label && } {hint &&

{hint}

}
{options.map((o) => { const active = selected.includes(o.value); return ( ); })}
); } export default function BecomeCoachPage() { const { t } = useTranslation(); useSeoMeta({ title: t('seoUi.becomeCoachTitle'), description: t('seoUi.becomeCoachDesc'), }); const navigate = useNavigate(); const { user, metadata } = useCurrentUser(); const author = useAuthor(user?.pubkey || ''); const { mutateAsync: createEvent, isPending: isPublishing } = useNostrPublish(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const { toast } = useToast(); const { webln, activeNWC } = useWallet(); // Check if user has any wallet connected const hasWallet = !!webln || !!activeNWC; // Optional "Pro" upgrade — free to list; Pro adds a badge + featured placement const { isActive: isPro, featuredUntilSeconds } = usePlatformFee('labProfile'); const [goPro, setGoPro] = useState(false); const [bio, setBio] = useState(''); const [location, setLocation] = useState(''); const [latitude, setLatitude] = useState(''); const [longitude, setLongitude] = useState(''); const [selectedSkills, setSelectedSkills] = useState([]); const [image, setImage] = useState(null); const [experience, setExperience] = useState(''); const [rate, setRate] = useState(''); const [rateAmount, setRateAmount] = useState(''); const [rateCurrency, setRateCurrency] = useState('sats'); const [rateUnit, setRateUnit] = useState('hour'); const [paymentMethods, setPaymentMethods] = useState([]); const [paymentNote, setPaymentNote] = useState(''); const [bookingFee, setBookingFee] = useState(''); const [bookingFeeCurrency, setBookingFeeCurrency] = useState('sats'); const [payWhatYouCan, setPayWhatYouCan] = useState(false); const [phone, setPhone] = useState(''); const [email, setEmail] = useState(''); const [website, setWebsite] = useState(''); const [instagram, setInstagram] = useState(''); const [noffer, setNoffer] = useState(''); const [formats, setFormats] = useState([]); const [levels, setLevels] = useState([]); const [ageGroups, setAgeGroups] = useState([]); const [credentials, setCredentials] = useState([]); const [certNote, setCertNote] = useState(''); const [languages, setLanguages] = useState([]); const [languageInput, setLanguageInput] = useState(''); const [availability, setAvailability] = useState([]); const [booking, setBooking] = useState(''); const [photos, setPhotos] = useState([]); const [video, setVideo] = useState(''); const [entityType, setEntityType] = useState('individual'); const [members, setMembers] = useState([]); const [crewRef, setCrewRef] = useState(undefined); const [selectedSpots, setSelectedSpots] = useState([]); const [isGettingLocation, setIsGettingLocation] = useState(false); const [titleInput, setTitleInput] = useState(''); const [prefilled, setPrefilled] = useState(false); const [previewOpen, setPreviewOpen] = useState(false); const [step, setStep] = useState(0); const draft = useLabDraft( `craft:coach`, { titleInput, bio, location, latitude, longitude, selectedSkills, image, experience, rate, rateAmount, rateCurrency, rateUnit, phone, email, website, instagram, noffer, entityType, members, crewRef, selectedSpots, formats, levels, ageGroups, credentials, certNote, languages, availability, booking, paymentMethods, paymentNote, bookingFee, bookingFeeCurrency, payWhatYouCan, photos, video }, (d) => { setTitleInput(d.titleInput); setBio(d.bio); setLocation(d.location); setLatitude(d.latitude); setLongitude(d.longitude); setSelectedSkills(d.selectedSkills); setImage(d.image); setExperience(d.experience); setRate(d.rate); setRateAmount(d.rateAmount); setRateCurrency(d.rateCurrency); setRateUnit(d.rateUnit); setPhone(d.phone); setEmail(d.email); setWebsite(d.website); setInstagram(d.instagram); setNoffer(d.noffer); setEntityType(d.entityType); setMembers(d.members); setCrewRef(d.crewRef); setSelectedSpots(d.selectedSpots); setFormats(d.formats); setLevels(d.levels); setAgeGroups(d.ageGroups); setCredentials(d.credentials); setCertNote(d.certNote); setLanguages(d.languages); setAvailability(d.availability); setBooking(d.booking); setPaymentMethods(d.paymentMethods); setPaymentNote(d.paymentNote); setBookingFee(d.bookingFee); setBookingFeeCurrency(d.bookingFeeCurrency); setPayWhatYouCan(d.payWhatYouCan); setPhotos(d.photos); setVideo(d.video); }, ); const [existingGeohash, setExistingGeohash] = useState(undefined); // Prefill from an existing profile so this is an edit, not a blind overwrite. const { data: existingProfile } = useMyLabProfileEvent(SKATE_COACH_KIND, 'coach-profile'); useEffect(() => { if (prefilled || !existingProfile) return; setPrefilled(true); const tag = (n: string) => existingProfile.tags.find(([t]) => t === n)?.[1]; const tags = (n: string) => existingProfile.tags.filter(([t]) => t === n).map(([, v]) => v); setTitleInput(tag('title') ?? ''); setExistingGeohash(tag('g')); setGoPro(existingProfile.tags.some(([t]) => t === 'featured')); setBio(existingProfile.content ?? ''); setLocation(tag('location') ?? ''); setImage(tag('image') ?? null); setExperience(tag('experience') ?? ''); setRate(tag('rate') ?? ''); setRateAmount(tag('rate_amount') ?? ''); setRateCurrency(tag('rate_currency') ?? 'sats'); setRateUnit(tag('rate_unit') ?? 'hour'); setPaymentMethods(tags('payment_method')); setPaymentNote(tag('payment_note') ?? ''); setBookingFee(tag('booking_fee') ?? ''); setBookingFeeCurrency(tag('booking_fee_currency') ?? 'sats'); setPayWhatYouCan(existingProfile.tags.some(([t, v]) => t === 'pay_what_you_can' && v === 'true')); setPhone(tag('phone') ?? ''); setEmail(tag('email') ?? ''); setWebsite(tag('website') ?? ''); setInstagram(tag('instagram') ?? ''); setNoffer(tag('noffer') ?? ''); setFormats(tags('format')); setLevels(tags('level')); setAgeGroups(tags('age')); setCredentials(tags('cert')); setCertNote(tag('cert_note') ?? ''); setLanguages(tags('language')); setAvailability(tags('availability')); setBooking(tag('booking') ?? ''); setPhotos(tags('photo')); setVideo(tag('video') ?? ''); setEntityType((tag('entity') as EntityType) ?? 'individual'); setMembers(tags('p')); setCrewRef(existingProfile.tags.find(([t, v]) => t === 'a' && v.startsWith('35006:'))?.[1]); setSelectedSkills(tags('t').filter((v) => v !== 'skatecoach')); setSelectedSpots( existingProfile.tags .filter(([t, v]) => t === 'a' && v.startsWith(`${SKATE_SPOT_KIND}:`)) .map(([, v]) => { const [, pk, ...rest] = v.split(':'); return `${pk}:${rest.join(':')}`; }), ); }, [existingProfile, prefilled]); const getDisplayName = () => { if (author.data?.metadata?.name) return author.data.metadata.name; if (user?.pubkey) { const npub = nip19.npubEncode(user.pubkey); return `${npub.slice(0, 12)}...${npub.slice(-4)}`; } return t('labUi.coachForm.anonymous'); }; const toggleSkill = (skill: string) => { setSelectedSkills((prev) => prev.includes(skill) ? prev.filter((s) => s !== skill) : [...prev, skill]); }; const toggleValue = (list: string[], setList: (v: string[]) => void, value: string) => { setList(list.includes(value) ? list.filter((v) => v !== value) : [...list, value]); }; const addLanguage = () => { const v = languageInput.trim(); if (!v) return; setLanguages((prev) => (prev.some((l) => l.toLowerCase() === v.toLowerCase()) ? prev : [...prev, v])); setLanguageInput(''); }; const getCurrentLocation = () => { if (!navigator.geolocation) { toast({ title: t('labUi.become.geolocationUnsupported'), 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('labUi.become.locationFound') }); }, (error) => { setIsGettingLocation(false); toast({ title: t('labUi.become.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) { setImage(url); toast({ title: t('labUi.become.imageUploaded') }); } } catch (error) { toast({ title: t('labUi.become.uploadFailed'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } e.target.value = ''; }; const handleGalleryUpload = async (files: File[]) => { const room = 12 - photos.length; if (room <= 0) return; try { const uploaded: string[] = []; for (const file of files.slice(0, room)) { const tags = await uploadFile(file); const url = tags[0]?.[1]; if (url) uploaded.push(url); } if (uploaded.length > 0) { setPhotos((prev) => [...prev, ...uploaded].slice(0, 12)); toast({ title: t('labUi.coachForm.galleryUploaded', { count: uploaded.length }) }); } } catch (error) { toast({ title: t('labUi.become.uploadFailed'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } }; // Optional, but if given it must be usable — a bad email/URL would otherwise // be stored and then silently hidden. const emailError = email.trim() && !isValidEmail(email) ? t('labUi.become.invalidEmail') : ''; const websiteError = website.trim() && !sanitizeUrl(normalizeUrlInput(website)) ? t('labUi.become.invalidWebsite') : ''; const contactError = emailError || websiteError; const [attempted, setAttempted] = useState(false); const scrollTo = (id: string) => document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Draft enrichment tags for the live preview (same tags the form publishes). const previewExtraTags: string[][] = []; for (const v of languages) previewExtraTags.push(['language', v]); for (const v of availability) previewExtraTags.push(['availability', v]); if (rateAmount.trim()) { previewExtraTags.push(['rate_amount', rateAmount.trim()], ['rate_currency', rateCurrency], ['rate_unit', rateUnit]); } for (const m of paymentMethods) previewExtraTags.push(['payment_method', m]); if (paymentNote.trim()) previewExtraTags.push(['payment_note', paymentNote.trim()]); if (bookingFee.trim()) previewExtraTags.push(['booking_fee', bookingFee.trim()], ['booking_fee_currency', bookingFeeCurrency]); if (payWhatYouCan) previewExtraTags.push(['pay_what_you_can', 'true']); for (const p of photos) previewExtraTags.push(['photo', p]); if (video.trim()) previewExtraTags.push(['video', video.trim()]); if (booking.trim()) previewExtraTags.push(['booking', booking.trim()]); const previewSkills = SKILL_ENTRIES .filter(([skill]) => selectedSkills.includes(skill)) .map(([skill, label]) => ({ label: COACH_SKILL_KEYS[skill] ? t(COACH_SKILL_KEYS[skill]) : label })); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setAttempted(true); if (contactError) { setStep(4); window.setTimeout(() => scrollTo(emailError ? 'email' : 'website'), 60); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } if (selectedSkills.length === 0) { setStep(1); window.setTimeout(() => scrollTo('coach-skills'), 60); toast({ title: t('labUi.coachForm.skillsRequired'), description: t('labUi.coachForm.selectSkill'), variant: 'destructive' }); return; } try { const identifier = 'coach-profile'; const displayName = getDisplayName(); const tags: string[][] = [ ['d', identifier], ['title', titleInput.trim() || displayName], ['t', 'skatecoach'], ['alt', `Skate coach: ${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)) { const geohash = ngeohash.encode(lat, lng, 7); tags.push(['g', geohash]); } } else if (existingGeohash) { // Preserve the existing map pin when editing without re-locating. tags.push(['g', existingGeohash]); } for (const skill of selectedSkills) tags.push(['t', skill]); if (image) tags.push(['image', image]); if (experience.trim()) tags.push(['experience', experience.trim()]); if (rate.trim()) tags.push(['rate', rate.trim()]); if (rateAmount.trim()) { tags.push(['rate_amount', rateAmount.trim()]); tags.push(['rate_currency', rateCurrency]); tags.push(['rate_unit', rateUnit]); } for (const m of paymentMethods) tags.push(['payment_method', m]); if (paymentNote.trim()) tags.push(['payment_note', paymentNote.trim()]); if (bookingFee.trim()) { tags.push(['booking_fee', bookingFee.trim()]); tags.push(['booking_fee_currency', bookingFeeCurrency]); } if (payWhatYouCan) tags.push(['pay_what_you_can', 'true']); if (phone.trim()) tags.push(['phone', phone.trim()]); if (email.trim()) tags.push(['email', email.trim()]); if (website.trim()) tags.push(['website', normalizeUrlInput(website)]); if (instagram.trim()) tags.push(['instagram', instagram.trim()]); if (isValidNoffer(noffer.trim())) tags.push(['noffer', noffer.trim().toLowerCase()]); for (const v of formats) tags.push(['format', v]); for (const v of levels) tags.push(['level', v]); for (const v of ageGroups) tags.push(['age', v]); for (const v of credentials) tags.push(['cert', v]); if (certNote.trim()) tags.push(['cert_note', certNote.trim()]); for (const v of languages) tags.push(['language', v]); for (const v of availability) tags.push(['availability', v]); if (booking.trim()) tags.push(['booking', normalizeUrlInput(booking)]); for (const p of photos) tags.push(['photo', p]); if (video.trim()) tags.push(['video', video.trim()]); tags.push(['entity', entityType]); if (entityType === 'collective') { for (const pk of members) tags.push(['p', pk]); if (crewRef) tags.push(['a', crewRef]); } // Add spot references for (const spotId of selectedSpots) { const [spotPubkey, spotIdentifier] = spotId.split(':'); tags.push(['a', `${SKATE_SPOT_KIND}:${spotPubkey}:${spotIdentifier}`]); } // Pro / featured listing (optional, paid) if (isPro && featuredUntilSeconds) { tags.push(['featured', String(featuredUntilSeconds)]); } await createEvent({ kind: SKATE_COACH_KIND, content: bio.trim(), tags, }); draft.clear(); toast({ title: existingProfile ? t('labUi.coachForm.updated') : t('labUi.coachForm.created'), description: t('labUi.become.profilePublished') }); navigate(`${labProfilePath('coach', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`); } catch (error) { toast({ title: t('labUi.become.failedToPublish'), description: error instanceof Error ? error.message : t('common.error'), variant: 'destructive' }); } }; if (!user) { return ( <>

{t('labUi.become.loginRequired')}

{t('labUi.become.loginRequiredDesc', { role: t('labUi.role.coach') })}

); } const hasNip05 = metadata?.nip05; return ( <>
{t('labUi.become.backToLab')}

{t('labUi.coachForm.heading')}

{t('labUi.coachForm.tagline')}

{!hasNip05 && ( )} {/* Wallet Requirement */}

{t('labUi.become.paymentSetup')}

{t('labUi.coachForm.paymentDesc')}

{hasWallet ? (

{t('labUi.become.walletConnected')}

{webln ? t('labUi.become.usingWebln') : t('labUi.become.usingNwc')}

) : (

{t('labUi.become.noWallet')}

{t('labUi.become.tipWithTime')}

)}
{draft.resumable && } 20} hasSkills={selectedSkills.length > 0} hasRate={!!(rate.trim() || rateAmount.trim())} hasContact={!!(email || website || phone || instagram)} hasLocation={!!location.trim()} />
{t('labUi.coachForm.profileTitle')} {t('labUi.coachForm.profileDesc')} setPreviewOpen(true)} className="h-12 flex-shrink-0 gap-2 border-gray-700 font-bold"> {t('labUi.preview.button')} } >
{metadata?.picture ? ( ) : ( )}

{getDisplayName()}

{t('labUi.become.fromNostrProfile')}

setTitleInput(e.target.value)} placeholder={getDisplayName()} maxLength={100} className="bg-gray-800 border-gray-700" />

{t('labUi.become.headlineHint', { example: t('labUi.coachForm.headlineExample') })}