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 (
);
}
/** 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()}
/>
>
);
}