SkateSpots/src/pages/BecomeEditorPage.tsx
SkateSpots c794d5fd18 Give Lab profiles a per-craft URL: /lab/<craft>/<pubkey>/<id>
Every provider lived at /coach/<pubkey>/<id> regardless of craft (a barber got
a "coach" URL). New src/lib/labRoutes.ts maps craft kind → segment and builds
the canonical path; the router adds /lab/:craft/:pubkey/:identifier (the craft
segment is cosmetic — the page still resolves by pubkey + identifier) and keeps
/coach/... working for old links. All generated links updated: offer-form
redirects, Lab cards + "Your Lab profiles", cross-links, spot/event/admin links,
the CVM client URL, the share path, and the SPA clean-link shim.
2026-10-04 11:32:15 +07:00

597 lines
29 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { 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: 'Become a Video Editor - SkateSpots',
description: 'Create your skateboarding video editor profile on SkateSpots.',
});
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<string[]>([]);
const [selectedSkills, setSelectedSkills] = useState<string[]>([]);
const [image, setImage] = useState<string | null>(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<EntityType>('individual');
const [members, setMembers] = useState<string[]>([]);
const [crewRef, setCrewRef] = useState<string | undefined>(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<string[][]>([]);
const [previewOpen, setPreviewOpen] = useState(false);
const [turnaround, setTurnaround] = useState('');
const [deliverables, setDeliverables] = useState('');
const [existingGeohash, setExistingGeohash] = useState<string | undefined>(undefined);
// Prefill from an existing profile so this is an edit, not a blind overwrite.
const { data: existingProfile } = useMyLabProfileEvent(SKATE_EDITOR_KIND, 'editor-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);
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<HTMLInputElement>) => {
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) {
toast({ title: t('labUi.become.checkContact'), description: contactError, variant: 'destructive' });
return;
}
if (selectedServices.length === 0) {
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 });
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 (
<>
<div className="container px-4 py-8">
<Card className="max-w-xl mx-auto bg-gray-900 border-gray-700">
<CardContent className="py-12 text-center">
<Scissors className="h-12 w-12 mx-auto mb-4 text-gray-600" />
<h2 className="text-xl font-bold mb-2">{t('labUi.become.loginRequired')}</h2>
<p className="text-gray-400 mb-6">{t('labUi.editorForm.loginDesc')}</p>
<LoginArea className="justify-center max-w-60 mx-auto" />
</CardContent>
</Card>
</div>
</>
);
}
const hasNip05 = !!author.data?.metadata?.nip05;
return (
<>
<div className="container px-4 sm:px-6 py-6 sm:py-8 max-w-2xl">
<div className="mb-6">
<Link to="/lab" className="inline-flex items-center text-gray-400 hover:text-white transition-colors mb-4">
<ArrowLeft className="h-4 w-4 mr-2" />
{t('labUi.become.backToLab')}
</Link>
<h1 className="text-3xl font-black uppercase" style={{ color: '#c084fc', fontFamily: 'Impact, "Arial Black", sans-serif', textShadow: '0 0 20px rgba(192,132,252,0.6)' }}>
{t('labUi.editorForm.heading')}
</h1>
<p className="text-gray-400 mt-1">{t('labUi.editorForm.tagline')}</p>
{!hasNip05 && (
<Card className="mt-4 bg-yellow-950/30 border-2 border-yellow-500/50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<span className="text-yellow-500">⚠️</span>
<div>
<h3 className="font-bold text-yellow-400 mb-1">{t('labUi.become.nip05Recommended')}</h3>
<p className="text-sm text-gray-300 mb-2">{t('labUi.become.nip05RecommendedDesc')}</p>
<Link to="/settings">
<Button variant="outline" size="sm" className="border-yellow-500/50 hover:bg-yellow-500/10 text-yellow-400 font-bold">
{t('labUi.become.addNip05')}
</Button>
</Link>
</div>
</div>
</CardContent>
</Card>
)}
<Card className="mt-4 bg-purple-950/30 border-2 border-purple-500/50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Wallet className="h-6 w-6 text-purple-400 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h3 className="font-bold text-purple-400 mb-2">{t('labUi.editorForm.walletTitle')}</h3>
<p className="text-sm text-gray-300 mb-3">{t('labUi.editorForm.walletDesc')}</p>
{hasWallet ? (
<div className="p-3 rounded-lg bg-green-950/30 border border-green-500/50">
<div className="flex items-center gap-2">
<div className="h-2 w-2 rounded-full bg-green-500" />
<p className="text-sm font-bold text-green-400">{t('labUi.editorForm.walletConnected')}</p>
</div>
</div>
) : (
<div className="flex flex-col sm:flex-row gap-2">
<WalletModal>
<Button className="gap-2 bg-purple-600 hover:bg-purple-700 flex-1">
<Wallet className="h-4 w-4" />
{t('labUi.become.connectWallet')}
</Button>
</WalletModal>
<a href="https://planb.academy/en/tutorials/wallet" target="_blank" rel="noopener noreferrer" className="flex-1">
<Button variant="outline" className="gap-2 w-full border-purple-500/50 text-purple-400 hover:bg-purple-500/10">
<ExternalLink className="h-4 w-4" /> {t('labUi.become.learnHow')}
</Button>
</a>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
<form onSubmit={handleSubmit}>
<Card className="bg-gray-900 border-gray-700">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Scissors className="h-5 w-5 text-purple-400" />
{t('labUi.editorForm.profileTitle')}
</CardTitle>
<CardDescription className="text-gray-400">{t('labUi.editorForm.profileDesc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Identity */}
<div className="space-y-2">
<Label>{t('labUi.editorForm.identity')}</Label>
<div className="p-4 rounded-lg border-2 border-purple-500/30 bg-purple-500/5">
<p className="font-black text-lg">{getDisplayName()}</p>
<p className="text-sm text-gray-400 mt-1">{t('labUi.become.fromNostrProfile')}</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title">{t('labUi.become.headline')}</Label>
<Input
id="title"
value={titleInput}
onChange={(e) => setTitleInput(e.target.value)}
placeholder={getDisplayName()}
maxLength={100}
className="bg-gray-800 border-gray-700"
/>
<p className="text-xs text-gray-500">
{t('labUi.become.headlineHint', { example: t('labUi.editorForm.headlineExample') })}
</p>
</div>
{/* Profile image */}
<div className="space-y-2">
<Label>{t('labUi.editorForm.thumbnail')}</Label>
{image ? (
<div className="relative w-32 h-32">
<img src={image} alt="profile" className="w-32 h-32 object-cover rounded-lg border-2 border-purple-500/40" />
<Button type="button" variant="destructive" size="icon" className="absolute -top-2 -right-2 h-6 w-6" onClick={() => setImage(null)}>
<X className="h-3 w-3" />
</Button>
</div>
) : (
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-gray-700 rounded-lg cursor-pointer hover:border-purple-500/50 transition-colors bg-gray-800/50">
<Upload className="h-8 w-8 text-gray-500 mb-2" />
<span className="text-sm text-gray-500">{isUploading ? t('labUi.become.uploading') : t('labUi.become.uploadImage')}</span>
<input type="file" className="hidden" accept="image/*" onChange={handleImageUpload} disabled={isUploading} />
</label>
)}
</div>
{/* Bio */}
<div className="space-y-2">
<Label htmlFor="bio">{t('labUi.become.bioWorkLabel')}</Label>
<Textarea id="bio" placeholder={t('labUi.editorForm.bioPlaceholder')} value={bio} onChange={(e) => setBio(e.target.value)} rows={5} maxLength={3000} className="bg-gray-800 border-gray-700" />
<p className="text-right text-xs text-gray-500">{bio.length}/3000</p>
</div>
<EntityTypeField
entityType={entityType}
onEntityTypeChange={setEntityType}
members={members}
onMembersChange={setMembers}
accentBg="bg-purple-600 hover:bg-purple-700"
accentText="text-purple-400"
/>
{entityType === 'collective' && (
<CrewLinkField value={crewRef} onChange={setCrewRef} accentBg="bg-purple-600 hover:bg-purple-700" accentText="text-purple-400" />
)}
{/* Edit Types */}
<div className="space-y-3">
<Label>{t('labUi.editorForm.editTypes')}</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{SERVICE_ENTRIES.map(([service, label]) => (
<button
key={service}
type="button"
onClick={() => toggleService(service)}
className={cn(
'p-3 rounded-lg text-left transition-all border text-sm',
selectedServices.includes(service)
? 'bg-purple-600 text-white border-purple-600'
: 'bg-gray-800 hover:bg-gray-700 border-gray-700'
)}
>
<div className="font-bold">{EDITOR_SERVICE_KEYS[service] ? t(EDITOR_SERVICE_KEYS[service]) : label}</div>
<div className="text-xs opacity-70 mt-1">{EDITOR_SERVICE_DESC_KEYS[service] ? t(EDITOR_SERVICE_DESC_KEYS[service]) : EDITOR_SERVICE_DESCRIPTIONS[service]}</div>
</button>
))}
</div>
</div>
{/* Software / Hardware Skills */}
<div className="space-y-3">
<Label>{t('labUi.editorForm.softwareHardware')}</Label>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{SKILL_ENTRIES.map(([skill, label]) => (
<button
key={skill}
type="button"
onClick={() => toggleSkill(skill)}
className={cn(
'p-2.5 rounded-lg text-left transition-all border text-sm',
selectedSkills.includes(skill)
? 'bg-purple-500 text-white border-purple-500'
: 'bg-gray-800 hover:bg-gray-700 border-gray-700'
)}
>
<div className="font-bold text-xs uppercase">{EDITOR_SKILL_KEYS[skill] ? t(EDITOR_SKILL_KEYS[skill]) : label}</div>
<div className="text-[10px] opacity-60 mt-0.5 line-clamp-1">{EDITOR_SKILL_DESC_KEYS[skill] ? t(EDITOR_SKILL_DESC_KEYS[skill]) : EDITOR_SKILL_DESCRIPTIONS[skill]}</div>
</button>
))}
</div>
</div>
{/* Hardware / Software free-text */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="hardware" className="flex items-center gap-2">
<Monitor className="h-4 w-4 text-gray-400" /> {t('labUi.editorForm.hardware')}
</Label>
<Input id="hardware" placeholder={t('labUi.editorForm.hardwarePlaceholder')} value={hardware} onChange={(e) => setHardware(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="software" className="flex items-center gap-2">
<Scissors className="h-4 w-4 text-gray-400" /> {t('labUi.editorForm.software')}
</Label>
<Input id="software" placeholder={t('labUi.editorForm.softwarePlaceholder')} value={software} onChange={(e) => setSoftware(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
</div>
{/* Experience & Rate */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="experience">{t('labUi.editorForm.yearsExperience')}</Label>
<Input id="experience" placeholder={t('labUi.editorForm.yearsPlaceholder')} value={experience} onChange={(e) => setExperience(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label htmlFor="rate">{t('labUi.editorForm.rate')}</Label>
<Input id="rate" placeholder={t('labUi.editorForm.ratePlaceholder')} value={rate} onChange={(e) => setRate(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
</div>
{/* Location */}
<div className="space-y-2">
<Label>{t('labUi.editorForm.locationOptional')}</Label>
<Input placeholder={t('labUi.become.cityCountry')} value={location} onChange={(e) => setLocation(e.target.value)} className="bg-gray-800 border-gray-700" />
<div className="grid grid-cols-2 gap-2">
<Input placeholder={t('labUi.become.latitude')} value={latitude} onChange={(e) => setLatitude(e.target.value)} className="bg-gray-800 border-gray-700 text-sm" />
<Input placeholder={t('labUi.become.longitude')} value={longitude} onChange={(e) => setLongitude(e.target.value)} className="bg-gray-800 border-gray-700 text-sm" />
</div>
<Button type="button" variant="outline" size="sm" onClick={getCurrentLocation} disabled={isGettingLocation} className="gap-2 border-gray-700">
{isGettingLocation ? <Loader2 className="h-4 w-4 animate-spin" /> : <MapPin className="h-4 w-4" />}
{t('labUi.become.useMyLocation')}
</Button>
</div>
{/* Contact & Portfolio */}
<div className="space-y-4">
<Label>{t('labUi.editorForm.contactPortfolio')}</Label>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input placeholder={t('labUi.become.email')} value={email} onChange={(e) => setEmail(e.target.value)} className="bg-gray-800 border-gray-700" />
<Input placeholder={t('labUi.editorForm.phoneTelegram')} value={phone} onChange={(e) => setPhone(e.target.value)} className="bg-gray-800 border-gray-700" />
<Input placeholder={t('labUi.editorForm.portfolioPlaceholder')} value={portfolio} onChange={(e) => setPortfolio(e.target.value)} className="bg-gray-800 border-gray-700" />
<Input placeholder={t('labUi.become.website')} value={website} onChange={(e) => setWebsite(e.target.value)} className="bg-gray-800 border-gray-700" />
<Input placeholder={t('labUi.editorForm.socialPlaceholder')} value={social} onChange={(e) => setSocial(e.target.value)} className="bg-gray-800 border-gray-700" />
</div>
{contactError && <p className="text-sm text-red-400">{contactError}</p>}
<PaymentReadinessNote />
<NofferField value={noffer} onChange={setNoffer} />
</div>
</CardContent>
</Card>
{/* Optional Pro upgrade — free to list without it */}
<div className="mt-4">
<Card className="bg-gray-900 border-gray-700">
<CardContent className="p-4 space-y-4">
<Label className="text-base font-bold">{t('labUi.craftFields.detailsTitle')}</Label>
<div className="space-y-2">
<Label>{t('labUi.craftFields.turnaround')}</Label>
<Input value={turnaround} onChange={(e) => setTurnaround(e.target.value)} placeholder="e.g. 2–5 days" className="bg-gray-800 border-gray-700" />
</div>
<div className="space-y-2">
<Label>{t('labUi.craftFields.deliverables')}</Label>
<Input value={deliverables} onChange={(e) => setDeliverables(e.target.value)} placeholder="e.g. 4K master, vertical cut, captions" className="bg-gray-800 border-gray-700" />
</div>
</CardContent>
</Card>
<LabExtrasFields
existingTags={existingProfile?.tags}
onChange={setExtraTags}
accentText="text-purple-400"
accentChip="bg-purple-600 text-white border-purple-600"
accentChipHover="hover:border-purple-500/60 hover:text-purple-400"
rateUnits={pickRateUnits('project', 'day', 'hour')}
/>
<LabProUpgrade enabled={goPro} onEnabledChange={setGoPro} accentText="text-purple-400" entityCoord={user ? `${SKATE_EDITOR_KIND}:${user.pubkey}:editor-profile` : undefined} />
</div>
<div className="mt-6 flex gap-3">
<Link to="/lab" className="flex-1">
<Button type="button" variant="outline" className="w-full border-gray-700">{t('common.cancel')}</Button>
<Button
type="button"
variant="outline"
onClick={() => setPreviewOpen(true)}
className="gap-2 border-gray-700 font-bold"
>
<Eye className="h-4 w-4" /> {t('labUi.preview.button')}
</Button>
</Link>
<Button
type="submit"
disabled={isPublishing || selectedServices.length === 0}
className="flex-1 bg-purple-600 hover:bg-purple-700 font-black uppercase gap-2"
>
{isPublishing ? (
<><Loader2 className="h-4 w-4 animate-spin" />{t('labUi.become.publishing')}</>
) : (
<><Scissors className="h-4 w-4" />{existingProfile ? t('labUi.editorForm.update') : isPro ? t('labUi.editorForm.createPro') : t('labUi.editorForm.create')}</>
)}
</Button>
</div>
</form>
</div>
<LabProfilePreview
open={previewOpen}
onOpenChange={setPreviewOpen}
heading={t('labUi.detail.noun.editor')}
noun={t('labUi.detail.noun.editor')}
accentText="text-purple-400"
accentChip="bg-purple-600 text-white border-purple-600"
icon={Scissors}
draft={{
title: titleInput.trim() || getDisplayName(),
bio, image, location, experience, rate,
extraTags,
contact: { phone, email, website, portfolio, social, noffer },
}}
/>
</>
);
}