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 { NofferField } from '@/components/NofferField'; import { PaymentReadinessNote } from '@/components/PaymentReadinessNote'; import { SKATE_EDITOR_KIND, EDITOR_SERVICE_LABELS, EDITOR_SERVICE_DESCRIPTIONS, EDITOR_SKILL_LABELS, EDITOR_SKILL_DESCRIPTIONS, isValidNoffer, type EntityType, } from '@/lib/types'; import { EDITOR_SERVICE_KEYS, EDITOR_SERVICE_DESC_KEYS, EDITOR_SKILL_KEYS, EDITOR_SKILL_DESC_KEYS } from '@/lib/labelKeys'; import { EntityTypeField } from '@/components/EntityTypeField'; import { CrewLinkField } from '@/components/CrewLinkField'; import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl'; import { useTranslation } from '@/contexts/language'; import { cn } from '@/lib/utils'; import { Scissors, Upload, X, Loader2, ArrowLeft, MapPin, Wallet, ExternalLink, Monitor, Eye } from 'lucide-react'; import { LabProUpgrade } from '@/components/LabProUpgrade'; import { usePlatformFee } from '@/hooks/usePlatformFee'; import { useMyLabProfileEvent } from '@/hooks/useMyLabProfile'; import { LabExtrasFields } from '@/components/LabExtrasFields'; 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 { pickRateUnits } from '@/lib/labExtras'; const SERVICE_ENTRIES = Object.entries(EDITOR_SERVICE_LABELS); const SKILL_ENTRIES = Object.entries(EDITOR_SKILL_LABELS); export default function BecomeEditorPage() { const { t } = useTranslation(); useSeoMeta({ title: t('seoUi.becomeEditorTitle'), description: t('seoUi.becomeEditorDesc'), }); const navigate = useNavigate(); const { user } = 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(); 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 [selectedServices, setSelectedServices] = useState([]); const [selectedSkills, setSelectedSkills] = useState([]); const [image, setImage] = useState(null); const [experience, setExperience] = useState(''); const [rate, setRate] = useState(''); const [phone, setPhone] = useState(''); const [email, setEmail] = useState(''); const [website, setWebsite] = useState(''); const [portfolio, setPortfolio] = useState(''); const [social, setSocial] = useState(''); const [noffer, setNoffer] = useState(''); const [entityType, setEntityType] = useState('individual'); const [members, setMembers] = useState([]); const [crewRef, setCrewRef] = useState(undefined); const [hardware, setHardware] = useState(''); const [software, setSoftware] = useState(''); const [isGettingLocation, setIsGettingLocation] = useState(false); const [titleInput, setTitleInput] = useState(''); const [prefilled, setPrefilled] = useState(false); const [extraTags, setExtraTags] = useState([]); const [previewOpen, setPreviewOpen] = useState(false); const [turnaround, setTurnaround] = useState(''); const [deliverables, setDeliverables] = useState(''); const [existingGeohash, setExistingGeohash] = useState(undefined); // Prefill from an existing profile so this is an edit, not a blind overwrite. const { data: existingProfile } = useMyLabProfileEvent(SKATE_EDITOR_KIND, 'editor-profile'); const draft = useLabDraft( `craft:editor`, { titleInput, bio, location, latitude, longitude, selectedServices, selectedSkills, image, experience, rate, phone, email, website, portfolio, social, noffer, entityType, members, crewRef, hardware, software, turnaround, deliverables, extraTags }, (d) => { setTitleInput(d.titleInput); setBio(d.bio); setLocation(d.location); setLatitude(d.latitude); setLongitude(d.longitude); setSelectedServices(d.selectedServices); setSelectedSkills(d.selectedSkills); setImage(d.image); setExperience(d.experience); setRate(d.rate); setPhone(d.phone); setEmail(d.email); setWebsite(d.website); setPortfolio(d.portfolio); setSocial(d.social); setNoffer(d.noffer); setEntityType(d.entityType); setMembers(d.members); setCrewRef(d.crewRef); setHardware(d.hardware); setSoftware(d.software); setTurnaround(d.turnaround); setDeliverables(d.deliverables); setExtraTags(d.extraTags); }, ); 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); const serviceKeys = new Set(Object.keys(EDITOR_SERVICE_LABELS)); const skillKeys = new Set(Object.keys(EDITOR_SKILL_LABELS)); const sel = tags('t').filter((v) => v !== 'skateeditor'); 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') ?? ''); setTurnaround(tag('turnaround') ?? ''); setDeliverables(tag('deliverables') ?? ''); setPhone(tag('phone') ?? ''); setEmail(tag('email') ?? ''); setWebsite(tag('website') ?? ''); setPortfolio(tag('portfolio') ?? ''); setSocial(tag('social') ?? ''); setNoffer(tag('noffer') ?? ''); setHardware(tag('hardware') ?? ''); setSoftware(tag('software') ?? ''); setEntityType((tag('entity') as EntityType) ?? 'individual'); setMembers(tags('p')); setCrewRef(existingProfile.tags.find(([t, v]) => t === 'a' && v.startsWith('35006:'))?.[1]); setSelectedServices(sel.filter((v) => serviceKeys.has(v))); setSelectedSkills(sel.filter((v) => skillKeys.has(v))); }, [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.editorForm.anonymous'); }; const toggleService = (s: string) => setSelectedServices((prev) => prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]); const toggleSkill = (s: string) => setSelectedSkills((prev) => prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]); const getCurrentLocation = () => { if (!navigator.geolocation) { toast({ title: t('labUi.become.geolocationUnsupported'), 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: t('labUi.become.locationFound') }); }, (err) => { setIsGettingLocation(false); toast({ title: t('labUi.become.locationError'), description: err.message, variant: 'destructive' }); } ); }; const handleImageUpload = async (e: React.ChangeEvent) => { const files = e.target.files; if (!files || files.length === 0) return; try { const tags = await uploadFile(files[0]); const url = tags[0]?.[1]; if (url) { setImage(url); toast({ title: t('labUi.become.imageUploaded') }); } } catch (err) { toast({ title: t('labUi.become.uploadFailed'), description: err instanceof Error ? err.message : t('common.error'), variant: 'destructive' }); } e.target.value = ''; }; // Optional, but if given it must be usable — a bad email/URL would otherwise // be stored and then silently hidden. const contactError = email.trim() && !isValidEmail(email) ? t('labUi.become.invalidEmail') : website.trim() && !sanitizeUrl(normalizeUrlInput(website)) ? t('labUi.become.invalidWebsite') : portfolio.trim() && !sanitizeUrl(normalizeUrlInput(portfolio)) ? t('labUi.become.invalidPortfolio') : ''; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (contactError) { document.getElementById('contact-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' }); return; } if (selectedServices.length === 0) { document.querySelector('form')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); toast({ title: t('labUi.editorForm.servicesRequired'), description: t('labUi.editorForm.selectService'), variant: 'destructive' }); return; } try { const identifier = 'editor-profile'; const displayName = getDisplayName(); const tags: string[][] = [ ['d', identifier], ['title', titleInput.trim() || displayName], ['t', 'skateeditor'], ['alt', `Skate video editor: ${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)]); } else if (existingGeohash) { // Preserve the existing map pin when editing without re-locating. tags.push(['g', existingGeohash]); } for (const s of selectedServices) tags.push(['t', s]); for (const s of selectedSkills) tags.push(['t', s]); if (image) tags.push(['image', image]); if (experience.trim()) tags.push(['experience', experience.trim()]); if (rate.trim()) tags.push(['rate', rate.trim()]); 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 (portfolio.trim()) tags.push(['portfolio', normalizeUrlInput(portfolio)]); if (social.trim()) tags.push(['social', normalizeUrlInput(social)]); if (isValidNoffer(noffer.trim())) tags.push(['noffer', noffer.trim().toLowerCase()]); tags.push(['entity', entityType]); for (const extra of extraTags) tags.push(extra); if (turnaround.trim()) tags.push(['turnaround', turnaround.trim()]); if (deliverables.trim()) tags.push(['deliverables', deliverables.trim()]); if (entityType === 'collective') { for (const pk of members) tags.push(['p', pk]); if (crewRef) tags.push(['a', crewRef]); } if (hardware.trim()) tags.push(['hardware', hardware.trim()]); if (software.trim()) tags.push(['software', software.trim()]); // Pro / featured listing (optional, paid) if (isPro && featuredUntilSeconds) { tags.push(['featured', String(featuredUntilSeconds)]); } await createEvent({ kind: SKATE_EDITOR_KIND, content: bio.trim(), tags }); draft.clear(); toast({ title: existingProfile ? t('labUi.editorForm.updated') : t('labUi.editorForm.created'), description: t('labUi.editorForm.published') }); navigate(`${labProfilePath('editor', user?.pubkey ?? '', identifier)}${existingProfile ? '' : '?published=1'}`); } catch (err) { toast({ title: t('labUi.become.failedToPublish'), description: err instanceof Error ? err.message : t('common.error'), variant: 'destructive' }); } }; if (!user) { return ( <>

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

{t('labUi.editorForm.loginDesc')}

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

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

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

{!hasNip05 && (
⚠️

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

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

)}

{t('labUi.editorForm.walletTitle')}

{t('labUi.editorForm.walletDesc')}

{hasWallet ? (

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

) : (
)}
{draft.resumable && } 20} hasSkills={selectedServices.length + selectedSkills.length > 0} hasRate={!!rate.trim()} hasContact={!!(email || website || phone || social)} hasLocation={!!location.trim()} />
{t('labUi.editorForm.profileTitle')} {t('labUi.editorForm.profileDesc')} {/* Identity */}

{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.editorForm.headlineExample') })}

{/* Profile image */}
{image ? (
profile
) : ( )}
{/* Bio */}